fastify-auto-sqlapi 0.1.2 → 0.1.4
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/AGENTS_BACKEND.md +41 -2
- package/AGENTS_FRONTEND.md +7 -5
- package/README.md +105 -5
- package/dist/bin/generate-schema.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/adapters/mysql-adapter.d.ts +4 -0
- package/dist/lib/adapters/mysql-adapter.d.ts.map +1 -1
- package/dist/lib/adapters/mysql-adapter.js +28 -17
- package/dist/lib/adapters/pg-adapter.d.ts +4 -0
- package/dist/lib/adapters/pg-adapter.d.ts.map +1 -1
- package/dist/lib/adapters/pg-adapter.js +18 -7
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +3 -1
- package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
- package/dist/lib/cli/pg-introspect.js +13 -4
- package/dist/lib/cli/schema-codegen.d.ts +2 -1
- package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
- package/dist/lib/cli/schema-codegen.js +17 -3
- package/dist/lib/cli/tables-codegen.d.ts +2 -0
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
- package/dist/lib/cli/tables-codegen.js +43 -12
- package/dist/lib/db.d.ts +17 -0
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +47 -0
- package/dist/lib/engine/bulk/bulk-delete.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-delete.js +25 -8
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +12 -9
- package/dist/lib/engine/rest/delete.d.ts.map +1 -1
- package/dist/lib/engine/rest/delete.js +9 -5
- package/dist/lib/engine/rest/get.js +1 -1
- package/dist/lib/engine/rest/insert.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.js +41 -34
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +74 -57
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +94 -34
- package/dist/lib/schema/bulk-delete.d.ts +1 -1
- package/dist/lib/schema/bulk-delete.d.ts.map +1 -1
- package/dist/lib/schema/bulk-delete.js +2 -2
- package/dist/lib/schema/bulk-upsert.d.ts +1 -1
- package/dist/lib/schema/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/schema/bulk-upsert.js +5 -3
- package/dist/lib/schema/helpers.d.ts +2 -0
- package/dist/lib/schema/helpers.d.ts.map +1 -1
- package/dist/lib/schema/helpers.js +3 -2
- package/dist/lib/schema/insert.d.ts.map +1 -1
- package/dist/lib/schema/insert.js +4 -1
- package/dist/lib/schema/update.d.ts.map +1 -1
- package/dist/lib/schema/update.js +4 -1
- package/dist/lib/sql-api.d.ts +2 -0
- package/dist/lib/sql-api.d.ts.map +1 -1
- package/dist/lib/sql-api.js +1 -0
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +10 -9
- package/dist/lib/tenant.d.ts +24 -2
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/lib/tenant.js +0 -0
- package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-delete.routes.js +3 -1
- package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-upsert.routes.js +3 -1
- package/dist/routes/auto/delete.routes.d.ts.map +1 -1
- package/dist/routes/auto/delete.routes.js +1 -0
- package/dist/routes/auto/get.routes.d.ts.map +1 -1
- package/dist/routes/auto/get.routes.js +1 -0
- package/dist/routes/auto/insert.routes.d.ts.map +1 -1
- package/dist/routes/auto/insert.routes.js +1 -0
- package/dist/routes/auto/route-helpers.d.ts +3 -1
- package/dist/routes/auto/route-helpers.d.ts.map +1 -1
- package/dist/routes/auto/route-helpers.js +3 -0
- package/dist/routes/auto/search.routes.d.ts.map +1 -1
- package/dist/routes/auto/search.routes.js +9 -0
- package/dist/routes/auto/update.routes.d.ts.map +1 -1
- package/dist/routes/auto/update.routes.js +1 -0
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/db.d.ts +11 -0
- package/dist/types/db.d.ts.map +1 -1
- package/dist/types/plugin.d.ts +16 -0
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/plugin.js +4 -1
- package/dist/types/schema.d.ts +2 -0
- package/dist/types/schema.d.ts.map +1 -1
- package/dist/types/search.d.ts +7 -0
- package/dist/types/search.d.ts.map +1 -1
- package/dist/types/table.d.ts +40 -2
- package/dist/types/table.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -20,15 +20,34 @@ export function parseSchemaFile(content) {
|
|
|
20
20
|
}
|
|
21
21
|
if (fields.length === 0)
|
|
22
22
|
return null;
|
|
23
|
+
// primaryKey: ["code"] — emitted by generate-schema from DB introspection
|
|
24
|
+
let primary;
|
|
25
|
+
const pkMatch = content.match(/primaryKey:\s*\[([^\]]*)\]/);
|
|
26
|
+
if (pkMatch) {
|
|
27
|
+
primary = pkMatch[1]
|
|
28
|
+
.split(',')
|
|
29
|
+
.map((s) => s.trim().replace(/^["']|["']$/g, ''))
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
if (primary.length === 0)
|
|
32
|
+
primary = undefined;
|
|
33
|
+
}
|
|
23
34
|
return {
|
|
24
35
|
schemaName: exportMatch[1],
|
|
25
36
|
tableName: tableNameMatch[1],
|
|
26
37
|
fields,
|
|
27
38
|
fieldTypes,
|
|
39
|
+
primary,
|
|
28
40
|
};
|
|
29
41
|
}
|
|
30
42
|
// ─── Detection ───────────────────────────────────────────────
|
|
31
43
|
function detectPrimaryKey(schema) {
|
|
44
|
+
// Preferred source: the PK introspected from the DB (primaryKey in the schema file).
|
|
45
|
+
if (schema.primary?.length) {
|
|
46
|
+
const pk = schema.primary.length === 1 ? schema.primary[0] : schema.primary;
|
|
47
|
+
const first = schema.primary[0];
|
|
48
|
+
return { pk, autoIncrement: schema.fieldTypes[first]?.includes('Optional') ?? false };
|
|
49
|
+
}
|
|
50
|
+
// Heuristic fallback for hand-written/legacy schema files without primaryKey.
|
|
32
51
|
if (schema.fields.includes('id')) {
|
|
33
52
|
return { pk: 'id', autoIncrement: schema.fieldTypes['id']?.includes('Optional') ?? false };
|
|
34
53
|
}
|
|
@@ -39,6 +58,18 @@ function detectPrimaryKey(schema) {
|
|
|
39
58
|
}
|
|
40
59
|
return { pk: schema.fields[0], autoIncrement: false };
|
|
41
60
|
}
|
|
61
|
+
/** First PK field: used for defaultOrder, relations and example snippets. */
|
|
62
|
+
function pkFirst(pk) {
|
|
63
|
+
return Array.isArray(pk) ? pk[0] : pk;
|
|
64
|
+
}
|
|
65
|
+
/** Render the `primary:` value for the defineTable template. */
|
|
66
|
+
function formatPrimary(pk) {
|
|
67
|
+
return Array.isArray(pk) ? `[${pk.map((p) => `'${p}'`).join(', ')}]` : `'${pk}'`;
|
|
68
|
+
}
|
|
69
|
+
/** All PK fields as an array, for "non-PK field" lookups in example snippets. */
|
|
70
|
+
function pkFields(pk) {
|
|
71
|
+
return Array.isArray(pk) ? pk : [pk];
|
|
72
|
+
}
|
|
42
73
|
export function detectRelations(schemas) {
|
|
43
74
|
const tableBySnakeName = new Map();
|
|
44
75
|
for (const schema of schemas) {
|
|
@@ -55,7 +86,7 @@ export function detectRelations(schemas) {
|
|
|
55
86
|
const { pk } = detectPrimaryKey(parent);
|
|
56
87
|
relations.push({
|
|
57
88
|
parentSchemaName: parent.schemaName,
|
|
58
|
-
parentField: pk,
|
|
89
|
+
parentField: pkFirst(pk),
|
|
59
90
|
childSchemaName: schema.schemaName,
|
|
60
91
|
childField: field,
|
|
61
92
|
});
|
|
@@ -113,7 +144,7 @@ export function generateSingleTableFile(schema, allSchemas) {
|
|
|
113
144
|
lines.push(``);
|
|
114
145
|
lines.push(`// Fields: ${schema.fields.join(', ')}`);
|
|
115
146
|
lines.push(`export const ${tableVarName} = defineTable({`);
|
|
116
|
-
lines.push(` primary:
|
|
147
|
+
lines.push(` primary: ${formatPrimary(pk)},`);
|
|
117
148
|
lines.push(` ...exportTableInfo(Schema),`);
|
|
118
149
|
// Commented example with extraFilters + extendedCondition
|
|
119
150
|
lines.push(` // ...exportTableInfo(`);
|
|
@@ -123,9 +154,9 @@ export function generateSingleTableFile(schema, allSchemas) {
|
|
|
123
154
|
lines.push(` // if (filters.q) condition.isILike(Schema.col('${schema.fields.includes('name') ? 'name' : schema.fields[0]}'), \`%\${filters.q}%\`);`);
|
|
124
155
|
lines.push(` // }`);
|
|
125
156
|
lines.push(` // ),`);
|
|
126
|
-
lines.push(` defaultOrder: '${pk}',`);
|
|
157
|
+
lines.push(` defaultOrder: '${pkFirst(pk)}',`);
|
|
127
158
|
if (autoIncrement) {
|
|
128
|
-
lines.push(` excludeFromCreation: ['${pk}'],`);
|
|
159
|
+
lines.push(` excludeFromCreation: ['${pkFirst(pk)}'],`);
|
|
129
160
|
}
|
|
130
161
|
else {
|
|
131
162
|
lines.push(` // excludeFromCreation: [],`);
|
|
@@ -140,14 +171,14 @@ export function generateSingleTableFile(schema, allSchemas) {
|
|
|
140
171
|
else {
|
|
141
172
|
lines.push(` // allowedReadJoins: [],`);
|
|
142
173
|
}
|
|
143
|
-
lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(Schema, ['${
|
|
174
|
+
lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(Schema, [${pkFields(pk).map((p) => `'${p}'`).join(', ')}])),`);
|
|
144
175
|
// schemaOverrides: suggest email format if email field exists, otherwise minLength on first string field
|
|
145
176
|
const emailField = schema.fields.find(f => f.toLowerCase().includes('email'));
|
|
146
177
|
if (emailField) {
|
|
147
178
|
lines.push(` // schemaOverrides: { ${emailField}: Type.String({ format: 'email' }) },`);
|
|
148
179
|
}
|
|
149
180
|
else {
|
|
150
|
-
const stringField = schema.fields.find(f => f
|
|
181
|
+
const stringField = schema.fields.find(f => !pkFields(pk).includes(f) && schema.fieldTypes[f]?.includes('String'));
|
|
151
182
|
if (stringField) {
|
|
152
183
|
lines.push(` // schemaOverrides: { ${stringField}: Type.String({ minLength: 1 }) },`);
|
|
153
184
|
}
|
|
@@ -156,7 +187,7 @@ export function generateSingleTableFile(schema, allSchemas) {
|
|
|
156
187
|
}
|
|
157
188
|
}
|
|
158
189
|
// validate: use first non-PK field for example
|
|
159
|
-
const exampleField = schema.fields.find(f =>
|
|
190
|
+
const exampleField = schema.fields.find(f => !pkFields(pk).includes(f)) || schema.fields[0];
|
|
160
191
|
lines.push(` validate: async (db, req, main, secondaries) => {`);
|
|
161
192
|
lines.push(` const errors: ValidationError[] = [];`);
|
|
162
193
|
lines.push(` // if (!main.${exampleField}) errors.push(['${exampleField}', 'required']);`);
|
|
@@ -225,11 +256,11 @@ export function generateTablesFile(schemas) {
|
|
|
225
256
|
lines.push(`// ─── ${schema.tableName} ──────────────────────────────`);
|
|
226
257
|
lines.push(`// Fields: ${schema.fields.join(', ')}`);
|
|
227
258
|
lines.push(`const ${tableVarName} = defineTable({`);
|
|
228
|
-
lines.push(` primary:
|
|
259
|
+
lines.push(` primary: ${formatPrimary(pk)},`);
|
|
229
260
|
lines.push(` ...exportTableInfo(${schema.schemaName}),`);
|
|
230
|
-
lines.push(` defaultOrder: '${pk}',`);
|
|
261
|
+
lines.push(` defaultOrder: '${pkFirst(pk)}',`);
|
|
231
262
|
if (autoIncrement) {
|
|
232
|
-
lines.push(` excludeFromCreation: ['${pk}'],`);
|
|
263
|
+
lines.push(` excludeFromCreation: ['${pkFirst(pk)}'],`);
|
|
233
264
|
}
|
|
234
265
|
else {
|
|
235
266
|
lines.push(` // excludeFromCreation: [],`);
|
|
@@ -244,9 +275,9 @@ export function generateTablesFile(schemas) {
|
|
|
244
275
|
else {
|
|
245
276
|
lines.push(` // allowedReadJoins: [],`);
|
|
246
277
|
}
|
|
247
|
-
lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(${schema.schemaName}, ['${
|
|
278
|
+
lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(${schema.schemaName}, [${pkFields(pk).map((p) => `'${p}'`).join(', ')}])),`);
|
|
248
279
|
lines.push(` // schemaOverrides: {},`);
|
|
249
|
-
const exampleFieldLegacy = schema.fields.find(f =>
|
|
280
|
+
const exampleFieldLegacy = schema.fields.find(f => !pkFields(pk).includes(f)) || schema.fields[0];
|
|
250
281
|
lines.push(` validate: async (db, req, main, secondaries) => {`);
|
|
251
282
|
lines.push(` const errors: ValidationError[] = [];`);
|
|
252
283
|
lines.push(` // if (!main.${exampleFieldLegacy}) errors.push(['${exampleFieldLegacy}', 'required']);`);
|
package/dist/lib/db.d.ts
CHANGED
|
@@ -13,10 +13,27 @@ export declare class QueryClient {
|
|
|
13
13
|
get qi(): (id: string) => string;
|
|
14
14
|
get ph(): (i: number) => string;
|
|
15
15
|
get dialectName(): string;
|
|
16
|
+
/** ConditionBuilder dialect ('postgres' | 'mysql') for per-instance builders. */
|
|
17
|
+
get cbDialect(): SqlDialect['cbDialect'];
|
|
18
|
+
/** Whether the dialect supports `RETURNING` (postgres/mariadb: yes, mysql: no). */
|
|
19
|
+
get supportsReturning(): boolean;
|
|
20
|
+
/** Build the ` RETURNING <quoted pk>` suffix ('' when unsupported). */
|
|
21
|
+
get returningPk(): SqlDialect['returningPk'];
|
|
16
22
|
get dateTrunc(): SqlDialect['dateTrunc'];
|
|
17
23
|
setDebug(mode: boolean): void;
|
|
18
24
|
expression(value: string): Expression;
|
|
19
25
|
query<T = Record<string, unknown>>(text: string, values?: unknown[]): Promise<SqlResult<T>>;
|
|
26
|
+
/**
|
|
27
|
+
* Run `fn` inside a DB transaction on a dedicated connection (BEGIN/COMMIT, with
|
|
28
|
+
* ROLLBACK + rethrow on error). Requires the underlying Queryable to expose
|
|
29
|
+
* `connect()`; when it does not (legacy custom adapters), `fn` runs directly on
|
|
30
|
+
* this client without transactional guarantees.
|
|
31
|
+
*
|
|
32
|
+
* Nested calls join the outer transaction: the tx QueryClient wraps a dedicated
|
|
33
|
+
* connection that has no `connect()`, so an inner `withTransaction` is a no-op
|
|
34
|
+
* wrapper around the same connection.
|
|
35
|
+
*/
|
|
36
|
+
withTransaction<T>(fn: (tx: QueryClient) => Promise<T>): Promise<T>;
|
|
20
37
|
insert(table: string, record: DbRecord, pkCol: string | string[]): Promise<Record<string, unknown>>;
|
|
21
38
|
insertOrUpdate(table: string, record: DbRecord, conflictKeys: string[], pkCol: string | string[]): Promise<Record<string, unknown>>;
|
|
22
39
|
bulkInsert(table: string, records: DbRecord[], pkCol: string | string[], chunkSize?: number): Promise<Record<string, unknown>[]>;
|
package/dist/lib/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAQ/C,sCAAsC;AACtC,eAAO,MAAM,WAAW,GAAI,GAAG,MAAM,WAA0B,CAAC;AAEhE,qBAAa,WAAW;;IACtB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,KAAK,CAAS;gBAEV,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,UAAU;IAKnD,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAE/B;IAED,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAE9B;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,SAAS,IAAI,UAAU,CAAC,WAAW,CAAC,CAEvC;IAED,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAI7B,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU;IAIrC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/db.js
CHANGED
|
@@ -23,6 +23,18 @@ export class QueryClient {
|
|
|
23
23
|
get dialectName() {
|
|
24
24
|
return this.dialect.name;
|
|
25
25
|
}
|
|
26
|
+
/** ConditionBuilder dialect ('postgres' | 'mysql') for per-instance builders. */
|
|
27
|
+
get cbDialect() {
|
|
28
|
+
return this.dialect.cbDialect;
|
|
29
|
+
}
|
|
30
|
+
/** Whether the dialect supports `RETURNING` (postgres/mariadb: yes, mysql: no). */
|
|
31
|
+
get supportsReturning() {
|
|
32
|
+
return this.dialect.supportsReturning;
|
|
33
|
+
}
|
|
34
|
+
/** Build the ` RETURNING <quoted pk>` suffix ('' when unsupported). */
|
|
35
|
+
get returningPk() {
|
|
36
|
+
return this.dialect.returningPk.bind(this.dialect);
|
|
37
|
+
}
|
|
26
38
|
get dateTrunc() {
|
|
27
39
|
return this.dialect.dateTrunc.bind(this.dialect);
|
|
28
40
|
}
|
|
@@ -39,6 +51,41 @@ export class QueryClient {
|
|
|
39
51
|
}
|
|
40
52
|
return this.client.query(text, values);
|
|
41
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Run `fn` inside a DB transaction on a dedicated connection (BEGIN/COMMIT, with
|
|
56
|
+
* ROLLBACK + rethrow on error). Requires the underlying Queryable to expose
|
|
57
|
+
* `connect()`; when it does not (legacy custom adapters), `fn` runs directly on
|
|
58
|
+
* this client without transactional guarantees.
|
|
59
|
+
*
|
|
60
|
+
* Nested calls join the outer transaction: the tx QueryClient wraps a dedicated
|
|
61
|
+
* connection that has no `connect()`, so an inner `withTransaction` is a no-op
|
|
62
|
+
* wrapper around the same connection.
|
|
63
|
+
*/
|
|
64
|
+
async withTransaction(fn) {
|
|
65
|
+
if (!this.client.connect)
|
|
66
|
+
return fn(this);
|
|
67
|
+
const conn = await this.client.connect();
|
|
68
|
+
const tx = new QueryClient(conn, this.dialect);
|
|
69
|
+
tx.setDebug(this.debug);
|
|
70
|
+
try {
|
|
71
|
+
await conn.query('BEGIN');
|
|
72
|
+
const result = await fn(tx);
|
|
73
|
+
await conn.query('COMMIT');
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
try {
|
|
78
|
+
await conn.query('ROLLBACK');
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// The connection is broken: surface the original error, not the rollback failure.
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await conn.release();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
42
89
|
// Quote a list of fields
|
|
43
90
|
#q(fields) {
|
|
44
91
|
return fields.map((f) => this.dialect.qi(f)).join(', ');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulk-delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-delete.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE5E,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"bulk-delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-delete.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE5E,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAiE5F"}
|
|
@@ -20,17 +20,34 @@ export async function bulkDeleteEngine(params) {
|
|
|
20
20
|
({ where, values } = buildTenantDeleteWhere(db, tableName, pkCol, ids, tenant));
|
|
21
21
|
}
|
|
22
22
|
else {
|
|
23
|
-
const cb = new ConditionBuilder('AND');
|
|
23
|
+
const cb = new ConditionBuilder('AND', db.cbDialect);
|
|
24
24
|
cb.isIn(db.qi(pkCol), ids);
|
|
25
25
|
where = cb.build(1, db.ph);
|
|
26
26
|
values = cb.getValues();
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
let results;
|
|
29
|
+
if (db.supportsReturning) {
|
|
30
|
+
const result = await db.query(`DELETE FROM ${db.qi(tableName)} WHERE ${where}${db.returningPk(db.qi(pkCol))}`, values);
|
|
31
|
+
// Happy path: everything requested was deleted
|
|
32
|
+
if (result.affectedRows === ids.length) {
|
|
33
|
+
results = ids.map((id) => ({ main: { [pk]: id } }));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// Partial delete: RETURNING tells exactly which rows went away
|
|
37
|
+
results = result.rows.map((r) => ({ main: { [pk]: r[pkCol] } }));
|
|
38
|
+
}
|
|
32
39
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
else {
|
|
41
|
+
// No RETURNING (mysql): affectedRows alone cannot tell WHICH ids were deleted on a
|
|
42
|
+
// partial match, so look up the rows visible through the same WHERE before deleting.
|
|
43
|
+
const existing = await db.query(`SELECT ${db.qi(pkCol)} AS pk FROM ${db.qi(tableName)} WHERE ${where}`, values);
|
|
44
|
+
await db.query(`DELETE FROM ${db.qi(tableName)} WHERE ${where}`, values);
|
|
45
|
+
results = existing.rows.map((r) => ({ main: { [pk]: r.pk } }));
|
|
46
|
+
}
|
|
47
|
+
// Called once with the ACTUALLY deleted ids (possibly a subset of the requested ones)
|
|
48
|
+
if (tableConf.afterBulkDelete && results.length > 0) {
|
|
49
|
+
const deletedIds = results.map((r) => r.main[pk]);
|
|
50
|
+
await tableConf.afterBulkDelete(db, request, deletedIds);
|
|
51
|
+
}
|
|
52
|
+
return results;
|
|
36
53
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-upsert.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-upsert.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAEjB,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAoF5F"}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import { camelcaseObject } from '../../naming.js';
|
|
1
2
|
import { processSecondaries, processDeletions, prepareInsertRecord } from '../write-helpers.js';
|
|
2
|
-
import { enforceTenantOnWrites } from '../../tenant.js';
|
|
3
|
+
import { enforceTenantOnWrites, assertTenantOwnsConflicts } from '../../tenant.js';
|
|
3
4
|
import { runBulkValidation } from '../validate.js';
|
|
4
|
-
import {
|
|
5
|
+
import { primaryAsCols } from '../../../types.js';
|
|
5
6
|
export async function bulkUpsertEngine(params) {
|
|
6
7
|
const { db, tableConf, dbTables, request, items, tenant } = params;
|
|
7
8
|
if (!items.length)
|
|
8
9
|
return [];
|
|
9
10
|
const schema = tableConf.Schema;
|
|
10
|
-
|
|
11
|
-
const pkCol = schema.col(
|
|
11
|
+
// Full primary key columns — composite PKs need every column (see insertEngine).
|
|
12
|
+
const pkCol = primaryAsCols(tableConf.primary, (f) => schema.col(f));
|
|
12
13
|
// 1. Bulk validation runs once for all items (skips per-item validate). Otherwise per-item
|
|
13
14
|
// validation happens inside prepareInsertRecord below.
|
|
14
15
|
if (tableConf.validateBulk) {
|
|
@@ -25,6 +26,8 @@ export async function bulkUpsertEngine(params) {
|
|
|
25
26
|
let pkRows;
|
|
26
27
|
if (upsertKeys) {
|
|
27
28
|
const conflictCols = upsertKeys.map((k) => schema.col(k));
|
|
29
|
+
// Tenant isolation: a conflicting upsert must not overwrite rows owned by another tenant.
|
|
30
|
+
await assertTenantOwnsConflicts(db, tenant, schema.tableName, conflictCols, preparedMains);
|
|
28
31
|
pkRows = await db.bulkInsertOrUpdate(schema.tableName, preparedMains, conflictCols, pkCol);
|
|
29
32
|
}
|
|
30
33
|
else {
|
|
@@ -34,12 +37,12 @@ export async function bulkUpsertEngine(params) {
|
|
|
34
37
|
const results = [];
|
|
35
38
|
for (let i = 0; i < items.length; i++) {
|
|
36
39
|
const item = items[i];
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
const result = { main:
|
|
40
|
+
// pkRows entries come from SQL RETURNING or mysql insertId synthesis — keys are DB
|
|
41
|
+
// column names; camelcaseObject maps them back to schema fields (full composite PK).
|
|
42
|
+
const mainPkCamel = camelcaseObject(pkRows[i], schema);
|
|
43
|
+
const result = { main: mainPkCamel };
|
|
41
44
|
// FK auto-fill: input camelCase + generated PK
|
|
42
|
-
const mainForFK = { ...inputMains[i],
|
|
45
|
+
const mainForFK = { ...inputMains[i], ...mainPkCamel };
|
|
43
46
|
if (item.secondaries && Object.keys(item.secondaries).length > 0) {
|
|
44
47
|
const sec = await processSecondaries(db, tableConf, dbTables, mainForFK, item.secondaries);
|
|
45
48
|
if (Object.keys(sec).length > 0)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/delete.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAY,MAAM,mBAAmB,CAAC;AAE9E,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/delete.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAY,MAAM,mBAAmB,CAAC;AAE9E,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAiC9E"}
|
|
@@ -17,11 +17,15 @@ export async function deleteEngine(params) {
|
|
|
17
17
|
const affectedRows = await db.delete(tableName, { [pkCol]: id });
|
|
18
18
|
if (affectedRows === 0)
|
|
19
19
|
throw httpError(404, `Record not found: ${id}`);
|
|
20
|
-
return { main: { [pk]: id } };
|
|
21
20
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
else {
|
|
22
|
+
const { where, values } = buildTenantDeleteWhere(db, tableName, pkCol, id, tenant);
|
|
23
|
+
const result = await db.query(`DELETE FROM ${db.qi(tableName)} WHERE ${where}`, values);
|
|
24
|
+
if (result.affectedRows === 0)
|
|
25
|
+
throw httpError(404, `Record not found: ${id}`);
|
|
26
|
+
}
|
|
27
|
+
if (tableConf.afterDelete) {
|
|
28
|
+
await tableConf.afterDelete(db, request, id);
|
|
29
|
+
}
|
|
26
30
|
return { main: { [pk]: id } };
|
|
27
31
|
}
|
|
@@ -6,7 +6,7 @@ import { primaryAsString } from '../../../types.js';
|
|
|
6
6
|
export async function getEngine(params) {
|
|
7
7
|
const { db, tableConf, id, tenant } = params;
|
|
8
8
|
const pkCol = tableConf.Schema.col(primaryAsString(tableConf.primary));
|
|
9
|
-
const cb = new ConditionBuilder('AND');
|
|
9
|
+
const cb = new ConditionBuilder('AND', db.cbDialect);
|
|
10
10
|
cb.isEqual(db.qi(pkCol), id);
|
|
11
11
|
const joins = [];
|
|
12
12
|
if (tenant) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/insert.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/insert.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAiE9E"}
|
|
@@ -1,42 +1,49 @@
|
|
|
1
1
|
import { camelcaseObject } from '../../naming.js';
|
|
2
2
|
import { processSecondaries, prepareInsertRecord } from '../write-helpers.js';
|
|
3
|
-
import { enforceTenantOnWrites } from '../../tenant.js';
|
|
4
|
-
import {
|
|
3
|
+
import { enforceTenantOnWrites, assertTenantOwnsConflicts } from '../../tenant.js';
|
|
4
|
+
import { primaryAsCols } from '../../../types.js';
|
|
5
5
|
export async function insertEngine(params) {
|
|
6
6
|
const { db, tableConf, dbTables, request, record, secondaries, tenant } = params;
|
|
7
7
|
const schema = tableConf.Schema;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
// Full primary key columns (composite PKs need every column for RETURNING /
|
|
9
|
+
// mysql synthesis, otherwise the response is missing PK fields).
|
|
10
|
+
const pkCol = primaryAsCols(tableConf.primary, (f) => schema.col(f));
|
|
10
11
|
// 1. Validate, run beforeInsert hook, snakecase, drop excluded fields.
|
|
11
12
|
const { camel: inputRecord, snake: mainRecord } = await prepareInsertRecord({ db, tableConf, request }, record, secondaries);
|
|
12
|
-
// 2
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
13
|
+
// Steps 2-5 are atomic: a failure in secondaries or afterInsert rolls back the
|
|
14
|
+
// main insert too. Degrades to non-transactional when the adapter has no connect().
|
|
15
|
+
return db.withTransaction(async (tx) => {
|
|
16
|
+
// 2. Tenant: inject (direct) or validate FK (indirect) — enforced after user mutations
|
|
17
|
+
await enforceTenantOnWrites(tx, tenant, [mainRecord]);
|
|
18
|
+
// 3. Insert main → returns PK-only
|
|
19
|
+
let pkResult;
|
|
20
|
+
const upsertKeys = tableConf.upsertMap?.get(schema);
|
|
21
|
+
if (upsertKeys) {
|
|
22
|
+
const conflictCols = upsertKeys.map((k) => schema.col(k));
|
|
23
|
+
// Tenant isolation: a conflicting upsert must not overwrite a row owned by another tenant.
|
|
24
|
+
await assertTenantOwnsConflicts(tx, tenant, schema.tableName, conflictCols, [mainRecord]);
|
|
25
|
+
pkResult = await tx.insertOrUpdate(schema.tableName, mainRecord, conflictCols, pkCol);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
pkResult = await tx.insert(schema.tableName, mainRecord, pkCol);
|
|
29
|
+
}
|
|
30
|
+
const mainPkCamel = camelcaseObject(pkResult, schema);
|
|
31
|
+
// 4. Secondaries: need full record for FK auto-fill (camelCase)
|
|
32
|
+
let secondaryResults;
|
|
33
|
+
if (secondaries && Object.keys(secondaries).length > 0) {
|
|
34
|
+
const mainForFK = { ...inputRecord, ...mainPkCamel };
|
|
35
|
+
secondaryResults = await processSecondaries(tx, tableConf, dbTables, mainForFK, secondaries);
|
|
36
|
+
}
|
|
37
|
+
// 5. afterInsert hook (camelCase — input merged with generated PK)
|
|
38
|
+
if (tableConf.afterInsert) {
|
|
39
|
+
const mainForHook = { ...inputRecord, ...mainPkCamel };
|
|
40
|
+
await tableConf.afterInsert(tx, request, mainForHook, secondaryResults);
|
|
41
|
+
}
|
|
42
|
+
// 6. Return PK-only
|
|
43
|
+
const result = { main: mainPkCamel };
|
|
44
|
+
if (secondaryResults && Object.keys(secondaryResults).length > 0) {
|
|
45
|
+
result.secondaries = secondaryResults;
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
});
|
|
42
49
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAwH9E"}
|