@nanobpm/urban 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -99,6 +99,10 @@ export interface EngineClient {
99
99
  processInstanceKey: string;
100
100
  variables?: Record<string, unknown>;
101
101
  }>;
102
+ /** Cancel a running process instance (the pages surface's row-cancel action). */
103
+ cancelInstance(input: {
104
+ processInstanceKey: string;
105
+ }): Promise<void>;
102
106
  /** Publish a message for correlation. */
103
107
  publishMessage(input: {
104
108
  name: string;
@@ -36,6 +36,14 @@ export interface SurfaceDecl {
36
36
  enabled?: boolean;
37
37
  path?: string;
38
38
  agent?: string;
39
+ /** pages surface: directory of `*.page.json` (relative to app root). Default `pages`. */
40
+ pagesDir?: string;
41
+ /** pages surface: the page served at `/`. Default `home`. */
42
+ homePage?: string;
43
+ /** pages surface: max rows a `dataGrid` fetch returns. Default 200. */
44
+ rowLimit?: number;
45
+ /** pages surface: the injected default datasource name. Default `app`. */
46
+ sourceName?: string;
39
47
  }
40
48
  export interface AppManifest {
41
49
  $schema?: string;
@@ -1,6 +1,7 @@
1
1
  import type { RuntimeContext } from "../context.ts";
2
2
  import type { SqliteDb } from "../host.ts";
3
3
  import type { DomainType } from "../manifest.ts";
4
+ import { Table, type DataSource as GatewayDataSource } from "./gateway.ts";
4
5
  /** A typed accessor over a table declared in manifest `types`. */
5
6
  export declare class TypeRepo {
6
7
  private readonly db;
@@ -20,6 +21,8 @@ export interface ProvisionedSource {
20
21
  readonly name: string;
21
22
  readonly driver: string;
22
23
  readonly db: SqliteDb;
24
+ /** The record-oriented gateway over this source — the RAD `Table<T>` surface (ADR 0055). */
25
+ readonly source: GatewayDataSource;
23
26
  readonly migrationsApplied: string[];
24
27
  close(): void;
25
28
  }
@@ -31,6 +34,12 @@ export declare class DataLayer {
31
34
  source(name?: string): ProvisionedSource;
32
35
  /** A typed accessor for a declared domain type (uses the default source). */
33
36
  repo(typeName: string, sourceName?: string): TypeRepo;
37
+ /** The record-oriented `DataSource` gateway for a source (the default when omitted) — the
38
+ * raw-SQL + `Table<T>` surface app handlers bind to (ADR 0055). */
39
+ open(sourceName?: string): GatewayDataSource;
40
+ /** A typed `Table<T>` gateway over one table on a source (the default when omitted). `pk` is
41
+ * the primary-key column (default "id"). */
42
+ table<T extends object = Record<string, unknown>>(name: string, pk?: string, sourceName?: string): Table<T>;
34
43
  describe(): Record<string, unknown>;
35
44
  closeAll(): void;
36
45
  }
@@ -2,6 +2,7 @@
2
2
  // `types` domain model. Only the `sqlite` driver is implemented; other drivers hit an
3
3
  // explicit seam (they are declared in the manifest but not yet provisioned). This is the
4
4
  // runtime side of ADR 0024 (datasource) + ADR 0040 (domain model), scoped to what an app needs.
5
+ import { makeGateway } from "./gateway.js";
5
6
  function sqlitePathFromUrl(url) {
6
7
  // Accept "file:./x.db", "file:x.db", "sqlite:./x.db" or a bare path.
7
8
  return url.replace(/^(file|sqlite):(\/\/)?/, "");
@@ -93,6 +94,16 @@ export class DataLayer {
93
94
  throw new Error(`no such type "${typeName}"`);
94
95
  return new TypeRepo(this.source(sourceName).db, typeName, def);
95
96
  }
97
+ /** The record-oriented `DataSource` gateway for a source (the default when omitted) — the
98
+ * raw-SQL + `Table<T>` surface app handlers bind to (ADR 0055). */
99
+ open(sourceName) {
100
+ return this.source(sourceName).source;
101
+ }
102
+ /** A typed `Table<T>` gateway over one table on a source (the default when omitted). `pk` is
103
+ * the primary-key column (default "id"). */
104
+ table(name, pk, sourceName) {
105
+ return this.open(sourceName).table(name, pk);
106
+ }
96
107
  describe() {
97
108
  return {
98
109
  default: this.defaultSource,
@@ -153,6 +164,7 @@ async function provisionSqlite(ctx, name, src) {
153
164
  name,
154
165
  driver: "sqlite",
155
166
  db,
167
+ source: makeGateway(db),
156
168
  migrationsApplied,
157
169
  close: () => db.close(),
158
170
  };
@@ -0,0 +1,81 @@
1
+ import type { SqliteDb } from "../host.ts";
2
+ export type Row = Record<string, unknown>;
3
+ export interface ExecResult {
4
+ /** Rows changed by an INSERT/UPDATE/DELETE. */
5
+ changed: number;
6
+ /** Rowid of the last inserted row, when the driver reports one. */
7
+ lastInsertId?: number | bigint;
8
+ }
9
+ /** One column of a table, from the datasource's introspected schema. */
10
+ export interface ColumnMeta {
11
+ name: string;
12
+ type: string;
13
+ notNull: boolean;
14
+ primaryKey: boolean;
15
+ }
16
+ /** One foreign-key constraint: `column` references `refTable(refColumn)`. `refColumn` is empty
17
+ * when the FK targets the parent's primary key without naming a column; `onDelete` is the
18
+ * referential action (e.g. `CASCADE`), empty when none was declared. */
19
+ export interface ForeignKeyMeta {
20
+ column: string;
21
+ refTable: string;
22
+ refColumn: string;
23
+ onDelete: string;
24
+ }
25
+ /** One table: its columns, index names, and foreign keys. Powers domain-type projection and
26
+ * the page runtime's list/detail binding. */
27
+ export interface TableMeta {
28
+ name: string;
29
+ columns: ColumnMeta[];
30
+ indexes: string[];
31
+ foreignKeys: ForeignKeyMeta[];
32
+ }
33
+ /** The one thin, uniform interface behind every driver (ADR 0024 §2) — the `TDataSet`
34
+ * equivalent. Every consumer shares exactly this surface, so the driver underneath is
35
+ * interchangeable. */
36
+ export interface DataSource {
37
+ /** Run a SELECT (or any row-returning statement) and collect the rows. */
38
+ query(sql: string, params?: unknown[]): Promise<Row[]>;
39
+ /** Run a non-row statement (INSERT/UPDATE/DELETE/DDL). */
40
+ exec(sql: string, params?: unknown[]): Promise<ExecResult>;
41
+ /** Run `fn` inside a transaction, committing on success and rolling back on throw. The
42
+ * handle passed to `fn` targets the same connection. */
43
+ tx<T>(fn: (t: DataSource) => Promise<T>): Promise<T>;
44
+ /** Introspect the datasource's tables/columns/indexes/foreign keys. */
45
+ schema(): Promise<TableMeta[]>;
46
+ /** A typed gateway over one table — the RAD "TTable". `pk` is the primary-key column
47
+ * (default "id"). */
48
+ table<T extends object = Row>(name: string, pk?: string): Table<T>;
49
+ }
50
+ /** Double-quote a SQL identifier (table/column), escaping embedded quotes. */
51
+ export declare function quoteIdent(name: string): string;
52
+ /** A typed gateway over a single table — the record-oriented data object a handler binds to
53
+ * instead of hand-writing SQL (the Delphi `TTable`/data-module idea, ADR 0029 §6). It builds
54
+ * parameterised SQL from a typed row object's own keys, so callers manipulate rows as records.
55
+ * `T` comes from a generated row type; this class is generic *runtime* and knows nothing about
56
+ * any specific schema. `pk` is the primary-key column (default `id`). */
57
+ export declare class Table<T extends object = Row> {
58
+ #private;
59
+ readonly name: string;
60
+ readonly pk: string;
61
+ constructor(src: DataSource, name: string, pk?: string);
62
+ /** Insert one row (only the present keys are written); returns the new primary-key value
63
+ * (the inserted rowid for an INTEGER PRIMARY KEY). */
64
+ insert(row: Partial<T>): Promise<number | bigint>;
65
+ /** Fetch the row with the given primary key, or `undefined`. */
66
+ get(id: unknown): Promise<T | undefined>;
67
+ /** Every row (optionally capped at `limit`). */
68
+ all(limit?: number): Promise<T[]>;
69
+ /** Rows matching an equality filter (keys ANDed). An empty filter matches all rows. */
70
+ find(where?: Partial<T>): Promise<T[]>;
71
+ /** The first row matching an equality filter, or `undefined`. */
72
+ findOne(where?: Partial<T>): Promise<T | undefined>;
73
+ /** Patch the row with the given primary key; returns rows changed. */
74
+ update(id: unknown, patch: Partial<T>): Promise<number>;
75
+ /** Delete the row with the given primary key; returns rows changed. */
76
+ delete(id: unknown): Promise<number>;
77
+ /** Count rows matching an equality filter (all rows when omitted). */
78
+ count(where?: Partial<T>): Promise<number>;
79
+ }
80
+ /** Wrap a provisioned `SqliteDb` as the record-oriented `DataSource` gateway. */
81
+ export declare function makeGateway(db: SqliteDb): DataSource;
@@ -0,0 +1,170 @@
1
+ // The record-oriented datasource gateway (ADR 0024 / ADR 0029 §6, ADR 0055 phase 1).
2
+ //
3
+ // This is the runtime port of the console-generated `data-sdk.ts` `DataSource` + `Table<T>`:
4
+ // the RAD "TTable" a handler binds to instead of hand-writing SQL. It sits on the runtime's
5
+ // synchronous `SqliteDb` host seam but keeps the *async* app-facing contract (every method
6
+ // returns a Promise), so an app-hosted worker/action/surface handler reaches typed records the
7
+ // same way regardless of which driver backs the source underneath.
8
+ //
9
+ // const db = app.data.open(); // the default source as a DataSource
10
+ // await db.exec("INSERT INTO orders(id) VALUES (?)", [id]);
11
+ // const orders = db.table<Order>("orders"); // a typed gateway over one table
12
+ // await orders.insert({ id, status: "new" });
13
+ // const o = await orders.get(id);
14
+ /** Double-quote a SQL identifier (table/column), escaping embedded quotes. */
15
+ export function quoteIdent(name) {
16
+ return `"${name.replaceAll('"', '""')}"`;
17
+ }
18
+ /** Build a parameterised ` WHERE a = ? AND b = ?` clause from an equality map; an empty map
19
+ * yields an empty clause (matches all rows). Takes `object` (not `Row`) so a `Partial<T>` for a
20
+ * generated `interface` row type (which lacks a string index signature) is accepted. */
21
+ function whereClause(where) {
22
+ const w = where;
23
+ const keys = Object.keys(w);
24
+ if (keys.length === 0)
25
+ return { clause: "", params: [] };
26
+ const clause = " WHERE " + keys.map((k) => `${quoteIdent(k)} = ?`).join(" AND ");
27
+ return { clause, params: keys.map((k) => w[k]) };
28
+ }
29
+ /** A typed gateway over a single table — the record-oriented data object a handler binds to
30
+ * instead of hand-writing SQL (the Delphi `TTable`/data-module idea, ADR 0029 §6). It builds
31
+ * parameterised SQL from a typed row object's own keys, so callers manipulate rows as records.
32
+ * `T` comes from a generated row type; this class is generic *runtime* and knows nothing about
33
+ * any specific schema. `pk` is the primary-key column (default `id`). */
34
+ export class Table {
35
+ name;
36
+ pk;
37
+ #src;
38
+ constructor(src, name, pk = "id") {
39
+ this.#src = src;
40
+ this.name = name;
41
+ this.pk = pk;
42
+ }
43
+ /** Insert one row (only the present keys are written); returns the new primary-key value
44
+ * (the inserted rowid for an INTEGER PRIMARY KEY). */
45
+ async insert(row) {
46
+ const keys = Object.keys(row);
47
+ if (keys.length === 0) {
48
+ throw new Error(`Table(${this.name}).insert: no columns to insert`);
49
+ }
50
+ const cols = keys.map(quoteIdent).join(", ");
51
+ const ph = keys.map(() => "?").join(", ");
52
+ const r = await this.#src.exec(`INSERT INTO ${quoteIdent(this.name)} (${cols}) VALUES (${ph})`, keys.map((k) => row[k]));
53
+ if (r.lastInsertId == null) {
54
+ // The driver reported no rowid — treat as a failed/ambiguous insert rather than
55
+ // silently returning 0, which a caller could mistake for a real primary key.
56
+ throw new Error(`Table(${this.name}).insert: driver reported no lastInsertId`);
57
+ }
58
+ return r.lastInsertId;
59
+ }
60
+ /** Fetch the row with the given primary key, or `undefined`. */
61
+ async get(id) {
62
+ const rows = await this.#src.query(`SELECT * FROM ${quoteIdent(this.name)} WHERE ${quoteIdent(this.pk)} = ? LIMIT 1`, [id]);
63
+ return rows[0];
64
+ }
65
+ /** Every row (optionally capped at `limit`). */
66
+ async all(limit) {
67
+ const lim = typeof limit === "number" && Number.isFinite(limit)
68
+ ? ` LIMIT ${Math.max(0, Math.floor(limit))}`
69
+ : "";
70
+ return (await this.#src.query(`SELECT * FROM ${quoteIdent(this.name)}${lim}`));
71
+ }
72
+ /** Rows matching an equality filter (keys ANDed). An empty filter matches all rows. */
73
+ async find(where = {}) {
74
+ const { clause, params } = whereClause(where);
75
+ return (await this.#src.query(`SELECT * FROM ${quoteIdent(this.name)}${clause}`, params));
76
+ }
77
+ /** The first row matching an equality filter, or `undefined`. */
78
+ async findOne(where = {}) {
79
+ const { clause, params } = whereClause(where);
80
+ const rows = await this.#src.query(`SELECT * FROM ${quoteIdent(this.name)}${clause} LIMIT 1`, params);
81
+ return rows[0];
82
+ }
83
+ /** Patch the row with the given primary key; returns rows changed. */
84
+ async update(id, patch) {
85
+ const keys = Object.keys(patch);
86
+ if (keys.length === 0)
87
+ return 0;
88
+ const set = keys.map((k) => `${quoteIdent(k)} = ?`).join(", ");
89
+ const r = await this.#src.exec(`UPDATE ${quoteIdent(this.name)} SET ${set} WHERE ${quoteIdent(this.pk)} = ?`, [...keys.map((k) => patch[k]), id]);
90
+ return r.changed;
91
+ }
92
+ /** Delete the row with the given primary key; returns rows changed. */
93
+ async delete(id) {
94
+ const r = await this.#src.exec(`DELETE FROM ${quoteIdent(this.name)} WHERE ${quoteIdent(this.pk)} = ?`, [id]);
95
+ return r.changed;
96
+ }
97
+ /** Count rows matching an equality filter (all rows when omitted). */
98
+ async count(where = {}) {
99
+ const { clause, params } = whereClause(where);
100
+ const rows = await this.#src.query(`SELECT COUNT(*) AS n FROM ${quoteIdent(this.name)}${clause}`, params);
101
+ return Number(rows[0]?.n ?? 0);
102
+ }
103
+ }
104
+ /** A `DataSource` implemented over the runtime's synchronous `SqliteDb` host seam. The methods
105
+ * are async to hold the app-facing contract; the work underneath is synchronous. */
106
+ class SqliteGateway {
107
+ #db;
108
+ constructor(db) {
109
+ this.#db = db;
110
+ }
111
+ async query(sql, params = []) {
112
+ return this.#db.all(sql, params);
113
+ }
114
+ async exec(sql, params = []) {
115
+ const r = this.#db.run(sql, params);
116
+ return { changed: Number(r.changes), lastInsertId: r.lastInsertRowid };
117
+ }
118
+ async tx(fn) {
119
+ this.#db.exec("BEGIN");
120
+ try {
121
+ const out = await fn(this);
122
+ this.#db.exec("COMMIT");
123
+ return out;
124
+ }
125
+ catch (e) {
126
+ this.#db.exec("ROLLBACK");
127
+ throw e;
128
+ }
129
+ }
130
+ async schema() {
131
+ // Exclude SQLite internals (`sqlite_%`) and Nano's own bookkeeping tables (`_urban_%` /
132
+ // `_nano_%`, e.g. the migrations ledger): neither is a user/domain table, so they must
133
+ // never surface in the domain model, DB Manager, or forms.
134
+ const tables = this.#db.all("SELECT name FROM sqlite_master WHERE type = 'table' " +
135
+ "AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '\\_urban\\_%' ESCAPE '\\' " +
136
+ "AND name NOT LIKE '\\_nano\\_%' ESCAPE '\\' ORDER BY name");
137
+ const out = [];
138
+ for (const t of tables) {
139
+ const cols = this.#db.all(`PRAGMA table_info(${quoteIdent(t.name)})`);
140
+ const idx = this.#db.all(`PRAGMA index_list(${quoteIdent(t.name)})`);
141
+ const fks = this.#db.all(`PRAGMA foreign_key_list(${quoteIdent(t.name)})`);
142
+ out.push({
143
+ name: t.name,
144
+ columns: cols.map((c) => ({
145
+ name: c.name,
146
+ type: c.type,
147
+ notNull: !!c.notnull,
148
+ primaryKey: !!c.pk,
149
+ })),
150
+ indexes: idx.map((i) => String(i.name)),
151
+ foreignKeys: fks.map((f) => ({
152
+ column: f.from,
153
+ refTable: f.table,
154
+ refColumn: f.to ?? "",
155
+ onDelete: f.on_delete && f.on_delete.toUpperCase() !== "NO ACTION"
156
+ ? f.on_delete.toUpperCase()
157
+ : "",
158
+ })),
159
+ });
160
+ }
161
+ return out;
162
+ }
163
+ table(name, pk = "id") {
164
+ return new Table(this, name, pk);
165
+ }
166
+ }
167
+ /** Wrap a provisioned `SqliteDb` as the record-oriented `DataSource` gateway. */
168
+ export function makeGateway(db) {
169
+ return new SqliteGateway(db);
170
+ }
@@ -0,0 +1,42 @@
1
+ import type { AppApi, RuntimeContext } from "../context.ts";
2
+ import type { EngineClient } from "../host.ts";
3
+ import { type Route } from "../router.ts";
4
+ /** The subset of the datasource gateway the page runtime needs. */
5
+ export interface PagesDataSource {
6
+ query(sql: string, params?: unknown[]): Promise<Record<string, unknown>[]>;
7
+ schema(): Promise<{
8
+ name: string;
9
+ }[]>;
10
+ }
11
+ export interface PagesOptions {
12
+ /** Directory holding `*.page.json` (relative to the app root). Default `pages`. */
13
+ pagesDir?: string;
14
+ /** The page served at `/`. Default `home`. */
15
+ homePage?: string;
16
+ /** Max rows a `dataGrid` fetch returns. Default 200. */
17
+ rowLimit?: number;
18
+ /** The injected default datasource name (the alias apps bind to). Default `app`. */
19
+ sourceName?: string;
20
+ }
21
+ export interface PagesDeps {
22
+ db: PagesDataSource;
23
+ engine: EngineClient;
24
+ /** Read a page file; injectable for tests. */
25
+ readPage(path: string): Promise<string>;
26
+ }
27
+ /**
28
+ * Build the page-runtime routes over an injected datasource + engine. Pure over its deps
29
+ * so it is unit-testable without a real host/server (see pages.test.ts).
30
+ */
31
+ export declare function createPagesRoutes(opts: PagesOptions, deps: PagesDeps): Route[];
32
+ export interface PagesHandle {
33
+ readonly name: string;
34
+ routes: Route[];
35
+ describe(): Record<string, unknown>;
36
+ }
37
+ /**
38
+ * Mount the `pages` surface when enabled in the manifest. Binds the page runtime to the
39
+ * app's datasource gateway (`app.data.open(source)`), engine, and the host's file read.
40
+ * Returns no routes when the surface is disabled or absent.
41
+ */
42
+ export declare function mountPages(ctx: RuntimeContext, app: AppApi): PagesHandle;