|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +import { SqlDriver } from '@objectstack/driver-sql'; |
| 4 | + |
| 5 | +/** |
| 6 | + * Runtime wiring for the external-datasource federation demo (ADR-0015). |
| 7 | + * |
| 8 | + * This is CODE, so it can't live in the declarative artifact — it runs from the |
| 9 | + * stack's `onEnable` hook, which the AppPlugin invokes at boot (Phase 2), before |
| 10 | + * the external-validation gate fires on `kernel:ready` (Phase 3). |
| 11 | + * |
| 12 | + * It does three things, all zero-config so `os dev` "just works": |
| 13 | + * 1. Idempotently provisions the "remote" SQLite file with `customers` / |
| 14 | + * `orders` tables + a little seed data (a MANAGED driver — DDL allowed). |
| 15 | + * 2. Registers a read-only `schemaMode: 'external'` driver for that same file |
| 16 | + * under the datasource name `showcase_external`, so ObjectQL can route |
| 17 | + * queries to it. (Declared datasources are surfaced in the metadata |
| 18 | + * registry for Setup → Datasources, but a live driver must be registered |
| 19 | + * for the federated objects to be queryable in standalone mode.) |
| 20 | + * 3. Registers the federated objects' read metadata (DDL-free) so coercion + |
| 21 | + * the remote-table mapping exist immediately. |
| 22 | + */ |
| 23 | + |
| 24 | +// Same relative path as the datasource config — resolved against the project |
| 25 | +// cwd by better-sqlite3. `connect()` creates the parent dir if missing. |
| 26 | +const EXTERNAL_DB_FILE = '.objectstack/data/showcase_external.db'; |
| 27 | + |
| 28 | +const CUSTOMER_TABLE = { |
| 29 | + name: 'customers', |
| 30 | + fields: { |
| 31 | + id: { type: 'text' }, |
| 32 | + name: { type: 'text' }, |
| 33 | + email: { type: 'text' }, |
| 34 | + region: { type: 'text' }, |
| 35 | + lifetime_value: { type: 'number' }, |
| 36 | + }, |
| 37 | +}; |
| 38 | + |
| 39 | +const ORDER_TABLE = { |
| 40 | + name: 'orders', |
| 41 | + fields: { |
| 42 | + id: { type: 'text' }, |
| 43 | + customer_id: { type: 'text' }, |
| 44 | + amount: { type: 'number' }, |
| 45 | + status: { type: 'text' }, |
| 46 | + placed_on: { type: 'date' }, |
| 47 | + }, |
| 48 | +}; |
| 49 | + |
| 50 | +const CUSTOMER_ROWS = [ |
| 51 | + { id: 'c1', name: 'Aurora Labs', email: 'ap@aurora.example', region: 'NA', lifetime_value: 480000 }, |
| 52 | + { id: 'c2', name: 'Borealis GmbH', email: 'billing@borealis.example', region: 'EU', lifetime_value: 312000 }, |
| 53 | + { id: 'c3', name: 'Cyan Pacific', email: 'accounts@cyan.example', region: 'APAC', lifetime_value: 95000 }, |
| 54 | +]; |
| 55 | + |
| 56 | +const ORDER_ROWS = [ |
| 57 | + { id: 'o1', customer_id: 'c1', amount: 12000, status: 'paid', placed_on: '2026-01-12' }, |
| 58 | + { id: 'o2', customer_id: 'c1', amount: 8400, status: 'paid', placed_on: '2026-02-03' }, |
| 59 | + { id: 'o3', customer_id: 'c2', amount: 21500, status: 'pending', placed_on: '2026-02-20' }, |
| 60 | + { id: 'o4', customer_id: 'c3', amount: 3300, status: 'paid', placed_on: '2026-03-01' }, |
| 61 | +]; |
| 62 | + |
| 63 | +/** Stack `onEnable` payload (subset we use). */ |
| 64 | +interface OnEnableContext { |
| 65 | + ql: { syncObjectSchema?: (name: string) => Promise<void> }; |
| 66 | + drivers: { register: (driver: unknown) => void }; |
| 67 | + logger?: { info?: (msg: string, meta?: unknown) => void; warn?: (msg: string, meta?: unknown) => void }; |
| 68 | +} |
| 69 | + |
| 70 | +export async function setupShowcaseExternalDatasource(ctx: OnEnableContext): Promise<void> { |
| 71 | + // 1. Provision the remote fixture with a MANAGED driver (DDL allowed). Idempotent. |
| 72 | + const fixture = new SqlDriver({ |
| 73 | + client: 'better-sqlite3', |
| 74 | + connection: { filename: EXTERNAL_DB_FILE }, |
| 75 | + useNullAsDefault: true, |
| 76 | + }) as unknown as { |
| 77 | + name: string; |
| 78 | + connect: () => Promise<void>; |
| 79 | + disconnect: () => Promise<void>; |
| 80 | + initObjects: (objs: unknown[]) => Promise<void>; |
| 81 | + count: (object: string, query: unknown) => Promise<number>; |
| 82 | + bulkCreate: (object: string, rows: unknown[]) => Promise<unknown>; |
| 83 | + }; |
| 84 | + fixture.name = 'showcase_external_fixture'; |
| 85 | + await fixture.connect(); |
| 86 | + try { |
| 87 | + await fixture.initObjects([CUSTOMER_TABLE, ORDER_TABLE]); |
| 88 | + if ((await fixture.count('customers', {})) === 0) { |
| 89 | + await fixture.bulkCreate('customers', CUSTOMER_ROWS); |
| 90 | + await fixture.bulkCreate('orders', ORDER_ROWS); |
| 91 | + } |
| 92 | + } finally { |
| 93 | + await fixture.disconnect(); |
| 94 | + } |
| 95 | + |
| 96 | + // 2. Register the read-only EXTERNAL driver under the datasource name. |
| 97 | + const ext = new SqlDriver({ |
| 98 | + client: 'better-sqlite3', |
| 99 | + connection: { filename: EXTERNAL_DB_FILE }, |
| 100 | + useNullAsDefault: true, |
| 101 | + schemaMode: 'external', |
| 102 | + } as never) as unknown as { name: string; connect: () => Promise<void> }; |
| 103 | + ext.name = 'showcase_external'; // MUST equal the datasource name (ObjectQL routes by driver name). |
| 104 | + await ext.connect(); |
| 105 | + ctx.drivers.register(ext); |
| 106 | + |
| 107 | + // 3. Register read metadata for the federated objects (DDL-free; ADR-0015). |
| 108 | + // Needed because the driver is registered after the boot schema-sync. |
| 109 | + await ctx.ql.syncObjectSchema?.('showcase_ext_customer'); |
| 110 | + await ctx.ql.syncObjectSchema?.('showcase_ext_order'); |
| 111 | + |
| 112 | + ctx.logger?.info?.('[showcase] external datasource "showcase_external" ready — federation demo (ADR-0015)'); |
| 113 | +} |
0 commit comments