@nanobpm/urban 0.16.0 → 0.16.1
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.
- package/dist/runtime/core/modules/datasource.js +10 -1
- package/dist/runtime/core/modules/gateway.d.ts +7 -3
- package/dist/runtime/core/modules/gateway.js +13 -6
- package/package.json +2 -2
- package/src/runtime/core/modules/datasource.ts +12 -1
- package/src/runtime/core/modules/gateway.test.ts +57 -0
- package/src/runtime/core/modules/gateway.ts +15 -6
- package/src/runtime/core/runtime.test.ts +6 -0
|
@@ -48,8 +48,17 @@ export class TypeRepo {
|
|
|
48
48
|
}
|
|
49
49
|
insert(row) {
|
|
50
50
|
this.assertFields(row);
|
|
51
|
-
|
|
51
|
+
// Omit keys whose value is `undefined` so the column's own `DEFAULT`/`NULL` governs;
|
|
52
|
+
// `undefined` means "not provided", never a bound value. An explicit `null` is preserved.
|
|
53
|
+
const provided = Object.keys(row);
|
|
54
|
+
const keys = provided.filter((k) => row[k] !== undefined);
|
|
52
55
|
if (keys.length === 0) {
|
|
56
|
+
// A present-but-all-undefined payload throws (parity with Table.insert): the caller
|
|
57
|
+
// meant to write those values, so silently inserting a full DEFAULT row would create
|
|
58
|
+
// an unintended record. A genuinely empty `insert({})` is the explicit "default row".
|
|
59
|
+
if (provided.length > 0) {
|
|
60
|
+
throw new Error(`insert into type "${this.typeName}": no columns to insert (all values were undefined)`);
|
|
61
|
+
}
|
|
53
62
|
return this.db.run(`INSERT INTO ${this.table()} DEFAULT VALUES`, []);
|
|
54
63
|
}
|
|
55
64
|
const cols = keys.join(", ");
|
|
@@ -59,8 +59,10 @@ export declare class Table<T extends object = Row> {
|
|
|
59
59
|
readonly name: string;
|
|
60
60
|
readonly pk: string;
|
|
61
61
|
constructor(src: DataSource, name: string, pk?: string);
|
|
62
|
-
/** Insert one row
|
|
63
|
-
*
|
|
62
|
+
/** Insert one row; returns the new primary-key value (the inserted rowid for an INTEGER
|
|
63
|
+
* PRIMARY KEY). Keys whose value is `undefined` are omitted, so the table's own column
|
|
64
|
+
* `DEFAULT`/`NULL` governs — `undefined` means "not provided, let the schema decide", never
|
|
65
|
+
* a bound value. An explicit `null` is preserved (it stores `NULL`). */
|
|
64
66
|
insert(row: Partial<T>): Promise<number | bigint>;
|
|
65
67
|
/** Fetch the row with the given primary key, or `undefined`. */
|
|
66
68
|
get(id: unknown): Promise<T | undefined>;
|
|
@@ -70,7 +72,9 @@ export declare class Table<T extends object = Row> {
|
|
|
70
72
|
find(where?: Partial<T>): Promise<T[]>;
|
|
71
73
|
/** The first row matching an equality filter, or `undefined`. */
|
|
72
74
|
findOne(where?: Partial<T>): Promise<T | undefined>;
|
|
73
|
-
/** Patch the row with the given primary key; returns rows changed.
|
|
75
|
+
/** Patch the row with the given primary key; returns rows changed. Keys whose value is
|
|
76
|
+
* `undefined` are skipped (that column is left unchanged); an explicit `null` clears the
|
|
77
|
+
* column to `NULL`. A patch with no defined keys is a no-op. */
|
|
74
78
|
update(id: unknown, patch: Partial<T>): Promise<number>;
|
|
75
79
|
/** Delete the row with the given primary key; returns rows changed. */
|
|
76
80
|
delete(id: unknown): Promise<number>;
|
|
@@ -40,12 +40,17 @@ export class Table {
|
|
|
40
40
|
this.name = name;
|
|
41
41
|
this.pk = pk;
|
|
42
42
|
}
|
|
43
|
-
/** Insert one row
|
|
44
|
-
*
|
|
43
|
+
/** Insert one row; returns the new primary-key value (the inserted rowid for an INTEGER
|
|
44
|
+
* PRIMARY KEY). Keys whose value is `undefined` are omitted, so the table's own column
|
|
45
|
+
* `DEFAULT`/`NULL` governs — `undefined` means "not provided, let the schema decide", never
|
|
46
|
+
* a bound value. An explicit `null` is preserved (it stores `NULL`). */
|
|
45
47
|
async insert(row) {
|
|
46
|
-
const
|
|
48
|
+
const provided = Object.keys(row);
|
|
49
|
+
const keys = provided.filter((k) => row[k] !== undefined);
|
|
47
50
|
if (keys.length === 0) {
|
|
48
|
-
throw new Error(
|
|
51
|
+
throw new Error(provided.length === 0
|
|
52
|
+
? `Table(${this.name}).insert: no columns to insert (empty row)`
|
|
53
|
+
: `Table(${this.name}).insert: no columns to insert (all values were undefined)`);
|
|
49
54
|
}
|
|
50
55
|
const cols = keys.map(quoteIdent).join(", ");
|
|
51
56
|
const ph = keys.map(() => "?").join(", ");
|
|
@@ -80,9 +85,11 @@ export class Table {
|
|
|
80
85
|
const rows = await this.#src.query(`SELECT * FROM ${quoteIdent(this.name)}${clause} LIMIT 1`, params);
|
|
81
86
|
return rows[0];
|
|
82
87
|
}
|
|
83
|
-
/** Patch the row with the given primary key; returns rows changed.
|
|
88
|
+
/** Patch the row with the given primary key; returns rows changed. Keys whose value is
|
|
89
|
+
* `undefined` are skipped (that column is left unchanged); an explicit `null` clears the
|
|
90
|
+
* column to `NULL`. A patch with no defined keys is a no-op. */
|
|
84
91
|
async update(id, patch) {
|
|
85
|
-
const keys = Object.keys(patch);
|
|
92
|
+
const keys = Object.keys(patch).filter((k) => patch[k] !== undefined);
|
|
86
93
|
if (keys.length === 0)
|
|
87
94
|
return 0;
|
|
88
95
|
const set = keys.map((k) => `${quoteIdent(k)} = ?`).join(", ");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"test:deno": "deno test -A --no-check src/"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@nanobpm/nano-sdk": "^1.2.
|
|
65
|
+
"@nanobpm/nano-sdk": "^1.2.3",
|
|
66
66
|
"@nanobpm/workflow": "^0.7.0",
|
|
67
67
|
"bpmn-auto-layout": "^2.0.0-alpha.2",
|
|
68
68
|
"create-urban-app": "^0.5.0"
|
|
@@ -60,8 +60,19 @@ export class TypeRepo {
|
|
|
60
60
|
|
|
61
61
|
insert(row: Record<string, unknown>): { changes: number; lastInsertRowid: number | bigint } {
|
|
62
62
|
this.assertFields(row);
|
|
63
|
-
|
|
63
|
+
// Omit keys whose value is `undefined` so the column's own `DEFAULT`/`NULL` governs;
|
|
64
|
+
// `undefined` means "not provided", never a bound value. An explicit `null` is preserved.
|
|
65
|
+
const provided = Object.keys(row);
|
|
66
|
+
const keys = provided.filter((k) => row[k] !== undefined);
|
|
64
67
|
if (keys.length === 0) {
|
|
68
|
+
// A present-but-all-undefined payload throws (parity with Table.insert): the caller
|
|
69
|
+
// meant to write those values, so silently inserting a full DEFAULT row would create
|
|
70
|
+
// an unintended record. A genuinely empty `insert({})` is the explicit "default row".
|
|
71
|
+
if (provided.length > 0) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`insert into type "${this.typeName}": no columns to insert (all values were undefined)`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
65
76
|
return this.db.run(`INSERT INTO ${this.table()} DEFAULT VALUES`, []);
|
|
66
77
|
}
|
|
67
78
|
const cols = keys.join(", ");
|
|
@@ -162,3 +162,60 @@ test("query/exec run raw parameterised SQL", async () => {
|
|
|
162
162
|
assert.equal(Number((rows[0] as { total: number }).total), 42);
|
|
163
163
|
});
|
|
164
164
|
});
|
|
165
|
+
|
|
166
|
+
test("insert omits undefined-valued keys so the column DEFAULT applies", async () => {
|
|
167
|
+
await withGateway(async (src) => {
|
|
168
|
+
const orders = src.table<Order>("orders");
|
|
169
|
+
// `total` is `NOT NULL DEFAULT 0`; passing it as undefined must omit the column
|
|
170
|
+
// (not bind NULL, which would violate NOT NULL), letting the DEFAULT fill it.
|
|
171
|
+
const id = await orders.insert({ status: "new", total: undefined });
|
|
172
|
+
assert.equal((await orders.get(id))?.total, 0);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("insert preserves an explicit null; omits undefined", async () => {
|
|
177
|
+
await withGateway(async (src) => {
|
|
178
|
+
await src.exec("CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT, tag TEXT DEFAULT 'none')");
|
|
179
|
+
const notes = src.table("notes");
|
|
180
|
+
const id = await notes.insert({ body: null, tag: undefined });
|
|
181
|
+
const row = await notes.get(id);
|
|
182
|
+
assert.equal(row?.body, null); // explicit null is stored as NULL
|
|
183
|
+
assert.equal(row?.tag, "none"); // undefined omitted → column DEFAULT
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("update skips undefined keys and clears on explicit null", async () => {
|
|
188
|
+
await withGateway(async (src) => {
|
|
189
|
+
await src.exec("CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT, tag TEXT DEFAULT 'none')");
|
|
190
|
+
const notes = src.table("notes");
|
|
191
|
+
const id = await notes.insert({ body: "hello", tag: "x" });
|
|
192
|
+
// tag:undefined must be left unchanged; body updated.
|
|
193
|
+
await notes.update(id, { tag: undefined, body: "world" });
|
|
194
|
+
let row = await notes.get(id);
|
|
195
|
+
assert.equal(row?.body, "world");
|
|
196
|
+
assert.equal(row?.tag, "x");
|
|
197
|
+
// explicit null clears the column.
|
|
198
|
+
await notes.update(id, { body: null });
|
|
199
|
+
row = await notes.get(id);
|
|
200
|
+
assert.equal(row?.body, null);
|
|
201
|
+
// a patch of only-undefined keys is a no-op.
|
|
202
|
+
assert.equal(await notes.update(id, { tag: undefined }), 0);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("insert throws when every provided value is undefined", async () => {
|
|
207
|
+
await withGateway(async (src) => {
|
|
208
|
+
const orders = src.table<Order>("orders");
|
|
209
|
+
await assert.rejects(
|
|
210
|
+
orders.insert({ status: undefined, total: undefined }),
|
|
211
|
+
/all values were undefined/,
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("insert throws a distinct message for a genuinely empty row", async () => {
|
|
217
|
+
await withGateway(async (src) => {
|
|
218
|
+
const orders = src.table<Order>("orders");
|
|
219
|
+
await assert.rejects(orders.insert({} as Partial<Order>), /empty row/);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -100,12 +100,19 @@ export class Table<T extends object = Row> {
|
|
|
100
100
|
this.pk = pk;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
/** Insert one row
|
|
104
|
-
*
|
|
103
|
+
/** Insert one row; returns the new primary-key value (the inserted rowid for an INTEGER
|
|
104
|
+
* PRIMARY KEY). Keys whose value is `undefined` are omitted, so the table's own column
|
|
105
|
+
* `DEFAULT`/`NULL` governs — `undefined` means "not provided, let the schema decide", never
|
|
106
|
+
* a bound value. An explicit `null` is preserved (it stores `NULL`). */
|
|
105
107
|
async insert(row: Partial<T>): Promise<number | bigint> {
|
|
106
|
-
const
|
|
108
|
+
const provided = Object.keys(row);
|
|
109
|
+
const keys = provided.filter((k) => (row as Row)[k] !== undefined);
|
|
107
110
|
if (keys.length === 0) {
|
|
108
|
-
throw new Error(
|
|
111
|
+
throw new Error(
|
|
112
|
+
provided.length === 0
|
|
113
|
+
? `Table(${this.name}).insert: no columns to insert (empty row)`
|
|
114
|
+
: `Table(${this.name}).insert: no columns to insert (all values were undefined)`,
|
|
115
|
+
);
|
|
109
116
|
}
|
|
110
117
|
const cols = keys.map(quoteIdent).join(", ");
|
|
111
118
|
const ph = keys.map(() => "?").join(", ");
|
|
@@ -158,9 +165,11 @@ export class Table<T extends object = Row> {
|
|
|
158
165
|
return rows[0] as T | undefined;
|
|
159
166
|
}
|
|
160
167
|
|
|
161
|
-
/** Patch the row with the given primary key; returns rows changed.
|
|
168
|
+
/** Patch the row with the given primary key; returns rows changed. Keys whose value is
|
|
169
|
+
* `undefined` are skipped (that column is left unchanged); an explicit `null` clears the
|
|
170
|
+
* column to `NULL`. A patch with no defined keys is a no-op. */
|
|
162
171
|
async update(id: unknown, patch: Partial<T>): Promise<number> {
|
|
163
|
-
const keys = Object.keys(patch);
|
|
172
|
+
const keys = Object.keys(patch).filter((k) => (patch as Row)[k] !== undefined);
|
|
164
173
|
if (keys.length === 0) return 0;
|
|
165
174
|
const set = keys.map((k) => `${quoteIdent(k)} = ?`).join(", ");
|
|
166
175
|
const r = await this.#src.exec(
|
|
@@ -152,6 +152,12 @@ test("runtime materializes the manifest end-to-end against a fake engine", async
|
|
|
152
152
|
|
|
153
153
|
// field-drift guard
|
|
154
154
|
assert.throws(() => app.data!.repo("task").insert({ bogus: 1 }));
|
|
155
|
+
// all-declared-but-undefined payload throws (parity with Table.insert): the caller
|
|
156
|
+
// meant to write those columns, so we don't silently insert a DEFAULT VALUES row.
|
|
157
|
+
assert.throws(
|
|
158
|
+
() => app.data!.repo("task").insert({ title: undefined, status: undefined }),
|
|
159
|
+
/all values were undefined/,
|
|
160
|
+
);
|
|
155
161
|
} finally {
|
|
156
162
|
await app.stop();
|
|
157
163
|
await rm(dir, { recursive: true, force: true });
|