fastify-auto-sqlapi 0.1.14 → 0.2.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.
Files changed (83) hide show
  1. package/AGENTS_BACKEND.md +4 -4
  2. package/BREAKING_CHANGES.md +5 -5
  3. package/README.md +1 -1
  4. package/dist/bin/generate-schema.js +89 -77
  5. package/dist/bin/generate-tables.js +72 -62
  6. package/dist/bin/utils.d.ts.map +1 -1
  7. package/dist/bin/utils.js +47 -33
  8. package/dist/lib/agent/manifest.d.ts +3 -2
  9. package/dist/lib/agent/manifest.d.ts.map +1 -1
  10. package/dist/lib/agent/manifest.js +102 -80
  11. package/dist/lib/cli/mysql-introspect.d.ts +4 -9
  12. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  13. package/dist/lib/cli/mysql-introspect.js +8 -7
  14. package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
  15. package/dist/lib/cli/pg-introspect.js +0 -1
  16. package/dist/lib/cli/schema-codegen.d.ts +3 -0
  17. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  18. package/dist/lib/cli/schema-codegen.js +3 -0
  19. package/dist/lib/cli/tables-codegen.d.ts +8 -1
  20. package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
  21. package/dist/lib/cli/tables-codegen.js +100 -118
  22. package/dist/lib/condition-methods.d.ts +14 -4
  23. package/dist/lib/condition-methods.d.ts.map +1 -1
  24. package/dist/lib/condition-methods.js +18 -4
  25. package/dist/lib/db.d.ts +1 -1
  26. package/dist/lib/db.d.ts.map +1 -1
  27. package/dist/lib/db.js +16 -4
  28. package/dist/lib/dialect.d.ts +2 -1
  29. package/dist/lib/dialect.d.ts.map +1 -1
  30. package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
  31. package/dist/lib/engine/bulk/bulk-upsert.js +42 -35
  32. package/dist/lib/engine/rest/delete.d.ts.map +1 -1
  33. package/dist/lib/engine/rest/get.d.ts.map +1 -1
  34. package/dist/lib/engine/rest/insert.d.ts.map +1 -1
  35. package/dist/lib/engine/rest/insert.js +1 -1
  36. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  37. package/dist/lib/engine/rest/update.js +25 -18
  38. package/dist/lib/engine/search/aggregations.d.ts +33 -0
  39. package/dist/lib/engine/search/aggregations.d.ts.map +1 -0
  40. package/dist/lib/engine/search/aggregations.js +109 -0
  41. package/dist/lib/engine/search/conditions.d.ts +31 -0
  42. package/dist/lib/engine/search/conditions.d.ts.map +1 -0
  43. package/dist/lib/engine/search/conditions.js +131 -0
  44. package/dist/lib/engine/search/fields.d.ts +49 -0
  45. package/dist/lib/engine/search/fields.d.ts.map +1 -0
  46. package/dist/lib/engine/search/fields.js +142 -0
  47. package/dist/lib/engine/search/join-families.d.ts +35 -0
  48. package/dist/lib/engine/search/join-families.d.ts.map +1 -0
  49. package/dist/lib/engine/search/join-families.js +345 -0
  50. package/dist/lib/engine/search/joins.d.ts +55 -0
  51. package/dist/lib/engine/search/joins.d.ts.map +1 -0
  52. package/dist/lib/engine/search/joins.js +81 -0
  53. package/dist/lib/engine/search/order-by.d.ts +35 -0
  54. package/dist/lib/engine/search/order-by.d.ts.map +1 -0
  55. package/dist/lib/engine/search/order-by.js +104 -0
  56. package/dist/lib/engine/search/search.d.ts.map +1 -1
  57. package/dist/lib/engine/search/search.js +101 -787
  58. package/dist/lib/engine/validate.d.ts +0 -5
  59. package/dist/lib/engine/validate.d.ts.map +1 -1
  60. package/dist/lib/engine/write-helpers.d.ts +25 -17
  61. package/dist/lib/engine/write-helpers.d.ts.map +1 -1
  62. package/dist/lib/engine/write-helpers.js +86 -44
  63. package/dist/lib/read-access.d.ts +12 -1
  64. package/dist/lib/read-access.d.ts.map +1 -1
  65. package/dist/lib/read-access.js +20 -4
  66. package/dist/lib/schema/helpers.d.ts +0 -15
  67. package/dist/lib/schema/helpers.d.ts.map +1 -1
  68. package/dist/lib/schema/helpers.js +7 -4
  69. package/dist/lib/schema/search.d.ts.map +1 -1
  70. package/dist/lib/schema/search.js +107 -128
  71. package/dist/lib/sql-api.d.ts.map +1 -1
  72. package/dist/lib/tenant.d.ts +26 -0
  73. package/dist/lib/tenant.d.ts.map +1 -1
  74. package/dist/lib/tenant.js +46 -0
  75. package/dist/routes/auto/route-helpers.d.ts +0 -7
  76. package/dist/routes/auto/route-helpers.d.ts.map +1 -1
  77. package/dist/routes/auto/route-helpers.js +46 -30
  78. package/dist/routes/auto/search.routes.js +0 -1
  79. package/dist/types/table.d.ts +1 -1
  80. package/docs/GLOSSARY.md +28 -0
  81. package/docs/adr/0012-anti-duplication-tooling.md +59 -0
  82. package/docs/adr/README.md +3 -1
  83. package/package.json +29 -2
@@ -1 +1 @@
1
- {"version":3,"file":"condition-methods.d.ts","sourceRoot":"","sources":["../../src/lib/condition-methods.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,wNAKvB,CAAC;AAEX,eAAO,MAAM,eAAe,wCAAyC,CAAC;AACtE,eAAO,MAAM,UAAU,8BAA+B,CAAC;AACvD,eAAO,MAAM,YAAY,kCAAmC,CAAC;AAE7D,eAAO,MAAM,eAAe,+RAElB,CAAC;AAGX,eAAO,MAAM,gBAAgB,aAAwC,CAAC;AACtE,eAAO,MAAM,WAAW,aAAmC,CAAC;AAC5D,eAAO,MAAM,MAAM,aAA8B,CAAC;AAClD,eAAO,MAAM,QAAQ,aAAgC,CAAC;AACtD,eAAO,MAAM,WAAW,aAAmC,CAAC"}
1
+ {"version":3,"file":"condition-methods.d.ts","sourceRoot":"","sources":["../../src/lib/condition-methods.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,eAAe,+RAElB,CAAC;AAGX,eAAO,MAAM,gBAAgB,aAAwC,CAAC;AACtE,eAAO,MAAM,WAAW,aAAmC,CAAC;AAC5D,eAAO,MAAM,MAAM,aAA8B,CAAC;AAClD,eAAO,MAAM,QAAQ,aAAgC,CAAC;AACtD,eAAO,MAAM,WAAW,aAAmC,CAAC;AAE5D;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC,yFAAyF;AACzF,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,+FAA+F;AAC/F,eAAO,MAAM,mBAAmB,OAAO,CAAC"}
@@ -1,14 +1,14 @@
1
1
  // Allowed ConditionBuilder methods for the `conditions` search API.
2
2
  // Single source of truth for both schema validation (Swagger) and runtime whitelist.
3
- export const SINGLE_VALUE_METHODS = [
3
+ const SINGLE_VALUE_METHODS = [
4
4
  'isEqual', 'isNotEqual',
5
5
  'isGreater', 'isNotGreater', 'isGreaterOrEqual', 'isNotGreaterOrEqual',
6
6
  'isLess', 'isNotLess', 'isLessOrEqual', 'isNotLessOrEqual',
7
7
  'isLike', 'isNotLike', 'isILike', 'isNotILike',
8
8
  ];
9
- export const BETWEEN_METHODS = ['isBetween', 'isNotBetween'];
10
- export const IN_METHODS = ['isIn', 'isNotIn'];
11
- export const NULL_METHODS = ['isNull', 'isNotNull'];
9
+ const BETWEEN_METHODS = ['isBetween', 'isNotBetween'];
10
+ const IN_METHODS = ['isIn', 'isNotIn'];
11
+ const NULL_METHODS = ['isNull', 'isNotNull'];
12
12
  export const ALLOWED_METHODS = [
13
13
  ...SINGLE_VALUE_METHODS, ...BETWEEN_METHODS, ...IN_METHODS, ...NULL_METHODS,
14
14
  ];
@@ -18,3 +18,17 @@ export const BETWEEN_SET = new Set(BETWEEN_METHODS);
18
18
  export const IN_SET = new Set(IN_METHODS);
19
19
  export const NULL_SET = new Set(NULL_METHODS);
20
20
  export const ALLOWED_SET = new Set(ALLOWED_METHODS);
21
+ /**
22
+ * Caps on how much work one search request may ask for.
23
+ *
24
+ * Every dotted condition and every 3-part `orderBy` token becomes its own correlated subquery,
25
+ * so an uncapped list turns a single request into arbitrarily many — `maxItemsPerPage` bounds
26
+ * the rows returned, not the work done to find them. They live here, in the data module both
27
+ * the engine and the TypeBox schema builders already share, so the request schema advertises
28
+ * exactly the limit the engine enforces.
29
+ */
30
+ export const MAX_CONDITIONS = 100;
31
+ /** Token cap the engine applies to `orderBy`, counted over its comma-separated parts. */
32
+ export const MAX_ORDER_BY_PARTS = 20;
33
+ /** Character cap the request schema applies to `orderBy` — generous for MAX_ORDER_BY_PARTS. */
34
+ export const MAX_ORDER_BY_LENGTH = 1024;
package/dist/lib/db.d.ts CHANGED
@@ -39,7 +39,7 @@ export declare class QueryClient {
39
39
  bulkInsert(table: string, records: DbRecord[], pkCol: string | string[], chunkSize?: number): Promise<Record<string, unknown>[]>;
40
40
  bulkInsertOrUpdate(table: string, records: DbRecord[], conflictKeys: string[], pkCol: string | string[], chunkSize?: number): Promise<Record<string, unknown>[]>;
41
41
  update(table: string, record: DbRecord, where: DbRecord, extraCondition?: ConditionBuilder): Promise<number>;
42
- delete(table: string, where: DbRecord): Promise<number>;
42
+ delete(table: string, where: DbRecord, extraCondition?: ConditionBuilder): Promise<number>;
43
43
  select<T = Record<string, unknown>>({ tableName, columns, where, values, limit, orderBy, joins, distinct, }: SelectOptions): Promise<T[]>;
44
44
  }
45
45
  export declare function createQueryClient(pool: Queryable, dialectName?: string): QueryClient;
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"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"}
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
@@ -221,16 +221,28 @@ export class QueryClient {
221
221
  extraWhere = ` AND ${extraCondition.build(values.length + 1, this.dialect.ph)}`;
222
222
  values.push(...extraCondition.getValues());
223
223
  }
224
+ const setClause = setF.map((f, i) => `${this.dialect.qi(f)} = ${setP[i]}`).join(', ');
225
+ const whereClause = whereF
226
+ .map((f, i) => `${this.dialect.qi(f)} = ${whereP[i]}`)
227
+ .join(' AND ');
224
228
  const result = await this.query(`UPDATE ${this.dialect.qi(table)}
225
- SET ${setF.map((f, i) => `${this.dialect.qi(f)} = ${setP[i]}`).join(', ')}
226
- WHERE ${whereF.map((f, i) => `${this.dialect.qi(f)} = ${whereP[i]}`).join(' AND ')}${extraWhere}`, values);
229
+ SET ${setClause}
230
+ WHERE ${whereClause}${extraWhere}`, values);
227
231
  return result.affectedRows;
228
232
  }
229
- async delete(table, where) {
233
+ async delete(table, where, extraCondition) {
230
234
  const values = [];
231
235
  const { fields, placeholders } = this.#params(where, values);
236
+ let extraWhere = '';
237
+ if (extraCondition) {
238
+ extraWhere = ` AND ${extraCondition.build(values.length + 1, this.dialect.ph)}`;
239
+ values.push(...extraCondition.getValues());
240
+ }
241
+ const whereClause = fields
242
+ .map((f, i) => `${this.dialect.qi(f)} = ${placeholders[i]}`)
243
+ .join(' AND ');
232
244
  const result = await this.query(`DELETE FROM ${this.dialect.qi(table)}
233
- WHERE ${fields.map((f, i) => `${this.dialect.qi(f)} = ${placeholders[i]}`).join(' AND ')}`, values);
245
+ WHERE ${whereClause}${extraWhere}`, values);
234
246
  return result.affectedRows;
235
247
  }
236
248
  async select({ tableName, columns = '*', where, values, limit = null, orderBy = '', joins = [], distinct = false, }) {
@@ -1,5 +1,5 @@
1
1
  export type DialectName = 'postgres' | 'mysql' | 'mariadb';
2
- export type TruncateUnit = 'year' | 'quarter' | 'month' | 'day' | 'hour';
2
+ type TruncateUnit = 'year' | 'quarter' | 'month' | 'day' | 'hour';
3
3
  export interface SqlDialect {
4
4
  name: DialectName;
5
5
  /** Quote identifier: "id" (postgres) or `id` (mysql/mariadb) */
@@ -31,4 +31,5 @@ export interface SqlDialect {
31
31
  dateTrunc(unit: TruncateUnit, qualifiedCol: string): string;
32
32
  }
33
33
  export declare function getDialect(name: DialectName): SqlDialect;
34
+ export {};
34
35
  //# sourceMappingURL=dialect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dialect.d.ts","sourceRoot":"","sources":["../../src/lib/dialect.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;AAE3D,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAEzE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,gEAAgE;IAChE,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,sDAAsD;IACtD,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,oCAAoC;IACpC,SAAS,EAAE,UAAU,GAAG,OAAO,CAAC;IAChC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,4CAA4C;IAC5C,SAAS,CAAC,kBAAkB,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC;IACvE,8CAA8C;IAC9C,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC;IACzC;;;;;;OAMG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAClE;;;;;;OAMG;IACH,SAAS,CAAC,IAAI,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7D;AAqFD,wBAAgB,UAAU,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,CAIxD"}
1
+ {"version":3,"file":"dialect.d.ts","sourceRoot":"","sources":["../../src/lib/dialect.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;AAE3D,KAAK,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAElE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,gEAAgE;IAChE,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,sDAAsD;IACtD,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,oCAAoC;IACpC,SAAS,EAAE,UAAU,GAAG,OAAO,CAAC;IAChC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,4CAA4C;IAC5C,SAAS,CAAC,kBAAkB,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC;IACvE,8CAA8C;IAC9C,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC;IACzC;;;;;;OAMG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAClE;;;;;;OAMG;IACH,SAAS,CAAC,IAAI,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7D;AAqFD,wBAAgB,UAAU,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,CAIxD"}
@@ -1 +1 @@
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
+ {"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-upsert.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,gBAAgB,EAChB,gBAAgB,EAEjB,MAAM,mBAAmB,CAAC;AA4D3B,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA0C5F"}
@@ -3,8 +3,45 @@ import { processSecondaries, processDeletions, prepareInsertRecord } from '../wr
3
3
  import { enforceTenantOnWrites, assertTenantOwnsConflicts } from '../../tenant.js';
4
4
  import { runBulkValidation } from '../validate.js';
5
5
  import { primaryAsCols } from '../../../types.js';
6
+ /** One bulk statement for every main row, returning the PKs only. */
7
+ function upsertMains(params, preparedMains, pkCol) {
8
+ const { db, tableConf, tenant } = params;
9
+ const schema = tableConf.Schema;
10
+ const upsertKeys = tableConf.upsertMap?.get(schema);
11
+ if (!upsertKeys) {
12
+ return db.bulkInsert(schema.tableName, preparedMains, pkCol);
13
+ }
14
+ const conflictCols = upsertKeys.map((k) => schema.col(k));
15
+ // Tenant isolation: a conflicting upsert must not overwrite rows owned by another tenant.
16
+ return assertTenantOwnsConflicts(db, tenant, schema.tableName, conflictCols, preparedMains)
17
+ .then(() => db.bulkInsertOrUpdate(schema.tableName, preparedMains, conflictCols, pkCol));
18
+ }
19
+ /** Secondaries, deletions and the afterInsert hook for one upserted item. */
20
+ async function finalizeUpsertedItem(params, item, inputMain, pkRow) {
21
+ const { db, tableConf, dbTables, request, tenant } = params;
22
+ // pkRow comes from SQL RETURNING or mysql insertId synthesis — keys are DB column names;
23
+ // camelcaseObject maps them back to schema fields (full composite PK).
24
+ const mainPkCamel = camelcaseObject(pkRow, tableConf.Schema);
25
+ const result = { main: mainPkCamel };
26
+ // FK auto-fill: input camelCase + generated PK
27
+ const mainForFK = { ...inputMain, ...mainPkCamel };
28
+ if (item.secondaries && Object.keys(item.secondaries).length > 0) {
29
+ const sec = await processSecondaries({ db, tableConf, dbTables, mainRecord: mainForFK, tenant }, item.secondaries);
30
+ if (Object.keys(sec).length > 0)
31
+ result.secondaries = sec;
32
+ }
33
+ if (item.deletions && Object.keys(item.deletions).length > 0) {
34
+ const del = await processDeletions({ db, tableConf, dbTables, mainRecord: mainForFK, tenant }, item.deletions);
35
+ if (Object.keys(del).length > 0)
36
+ result.deletions = del;
37
+ }
38
+ if (tableConf.afterInsert) {
39
+ await tableConf.afterInsert(db, request, mainForFK, result.secondaries);
40
+ }
41
+ return result;
42
+ }
6
43
  export async function bulkUpsertEngine(params) {
7
- const { db, tableConf, dbTables, request, items, tenant } = params;
44
+ const { db, tableConf, request, items, tenant } = params;
8
45
  if (!items.length)
9
46
  return [];
10
47
  const schema = tableConf.Schema;
@@ -22,42 +59,12 @@ export async function bulkUpsertEngine(params) {
22
59
  // 3. Tenant: inject (direct) or batch-validate FK (indirect) — enforced after user mutations
23
60
  await enforceTenantOnWrites(db, tenant, preparedMains);
24
61
  // 4. Bulk upsert all mains in one query → returns PK-only
25
- const upsertKeys = tableConf.upsertMap?.get(schema);
26
- let pkRows;
27
- if (upsertKeys) {
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);
31
- pkRows = await db.bulkInsertOrUpdate(schema.tableName, preparedMains, conflictCols, pkCol);
32
- }
33
- else {
34
- pkRows = await db.bulkInsert(schema.tableName, preparedMains, pkCol);
35
- }
36
- // 5. Process secondaries, deletions, afterInsert per item (all camelCase)
62
+ const pkRows = await upsertMains(params, preparedMains, pkCol);
63
+ // 5-6. Secondaries, deletions and afterInsert per item — sequential on purpose, so hooks
64
+ // observe the same order the caller sent, and errors surface on the first bad item.
37
65
  const results = [];
38
66
  for (let i = 0; i < items.length; i++) {
39
- const item = items[i];
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 };
44
- // FK auto-fill: input camelCase + generated PK
45
- const mainForFK = { ...inputMains[i], ...mainPkCamel };
46
- if (item.secondaries && Object.keys(item.secondaries).length > 0) {
47
- const sec = await processSecondaries(db, tableConf, dbTables, mainForFK, item.secondaries);
48
- if (Object.keys(sec).length > 0)
49
- result.secondaries = sec;
50
- }
51
- if (item.deletions && Object.keys(item.deletions).length > 0) {
52
- const del = await processDeletions(db, tableConf, mainForFK, item.deletions);
53
- if (Object.keys(del).length > 0)
54
- result.deletions = del;
55
- }
56
- // 6. afterInsert hook per item (camelCase)
57
- if (tableConf.afterInsert) {
58
- await tableConf.afterInsert(db, request, mainForFK, result.secondaries);
59
- }
60
- results.push(result);
67
+ results.push(await finalizeUpsertedItem(params, items[i], inputMains[i], pkRows[i]));
61
68
  }
62
69
  return results;
63
70
  }
@@ -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,CAuC9E"}
1
+ {"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/delete.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEpE,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAuC9E"}
@@ -1 +1 @@
1
- {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/get.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAuB,MAAM,mBAAmB,CAAC;AAEnF,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAsCrE"}
1
+ {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/get.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9D,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAsCrE"}
@@ -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,CAiE9E"}
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,CAmE9E"}
@@ -32,7 +32,7 @@ export async function insertEngine(params) {
32
32
  let secondaryResults;
33
33
  if (secondaries && Object.keys(secondaries).length > 0) {
34
34
  const mainForFK = { ...inputRecord, ...mainPkCamel };
35
- secondaryResults = await processSecondaries(tx, tableConf, dbTables, mainForFK, secondaries);
35
+ secondaryResults = await processSecondaries({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant }, secondaries);
36
36
  }
37
37
  // 5. afterInsert hook (camelCase — input merged with generated PK)
38
38
  if (tableConf.afterInsert) {
@@ -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,CAwH9E"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAiC3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA2G9E"}
@@ -3,6 +3,28 @@ import { processSecondaries, processDeletions } from '../write-helpers.js';
3
3
  import { stripTenantColumn, assertTenantOwnership, buildTenantUpdateExtra, enforceTenantFKOnUpdate, } from '../../tenant.js';
4
4
  import { runValidation } from '../validate.js';
5
5
  import { httpError } from '../../errors.js';
6
+ /**
7
+ * Confirm the row exists — and is visible to this tenant — when the request carries no
8
+ * updatable fields. Secondaries and deletions still need the main row to be there, and a
9
+ * missing one must be a 404 rather than a silent success.
10
+ */
11
+ async function assertRecordExists(tx, tableName, pkCols, pkValues, extraCondition) {
12
+ const whereParts = pkCols.map((c, i) => `${tx.qi(tableName)}.${tx.qi(c)} = ${tx.ph(i + 1)}`);
13
+ let whereSql = whereParts.join(' AND ');
14
+ const whereValues = [...pkValues];
15
+ if (extraCondition) {
16
+ whereSql += ` AND ${extraCondition.build(pkCols.length + 1, tx.ph)}`;
17
+ whereValues.push(...extraCondition.getValues());
18
+ }
19
+ const rows = await tx.select({
20
+ tableName,
21
+ where: whereSql,
22
+ values: whereValues,
23
+ limit: '1',
24
+ });
25
+ if (rows.length === 0)
26
+ throw httpError(404, `Record not found`);
27
+ }
6
28
  export async function updateEngine(params) {
7
29
  const { db, tableConf, dbTables, request, record, secondaries, deletions, tenant } = params;
8
30
  const schema = tableConf.Schema;
@@ -50,22 +72,7 @@ export async function updateEngine(params) {
50
72
  throw httpError(404, `Record not found`);
51
73
  }
52
74
  else {
53
- // No fields to update: verify the record exists (for secondaries/deletions)
54
- const whereParts = pkCols.map((c, i) => `${tx.qi(schema.tableName)}.${tx.qi(c)} = ${tx.ph(i + 1)}`);
55
- let whereSql = whereParts.join(' AND ');
56
- const whereValues = [...pkValues];
57
- if (extraCondition) {
58
- whereSql += ` AND ${extraCondition.build(pkCols.length + 1, tx.ph)}`;
59
- whereValues.push(...extraCondition.getValues());
60
- }
61
- const rows = await tx.select({
62
- tableName: schema.tableName,
63
- where: whereSql,
64
- values: whereValues,
65
- limit: '1',
66
- });
67
- if (rows.length === 0)
68
- throw httpError(404, `Record not found`);
75
+ await assertRecordExists(tx, schema.tableName, pkCols, pkValues, extraCondition);
69
76
  }
70
77
  // Build the main response (PK-only — every PK field)
71
78
  const mainResult = Object.fromEntries(pkFields.map((f, i) => [f, pkValues[i]]));
@@ -73,13 +80,13 @@ export async function updateEngine(params) {
73
80
  let secondaryResults;
74
81
  if (secondaries && Object.keys(secondaries).length > 0) {
75
82
  const mainForFK = { ...inputRecord };
76
- secondaryResults = await processSecondaries(tx, tableConf, dbTables, mainForFK, secondaries);
83
+ secondaryResults = await processSecondaries({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant }, secondaries);
77
84
  }
78
85
  // 9. Deletions (FK auto-fill from main like secondaries)
79
86
  let deletionResults;
80
87
  if (deletions && Object.keys(deletions).length > 0) {
81
88
  const mainForFK = { ...inputRecord };
82
- deletionResults = await processDeletions(tx, tableConf, mainForFK, deletions);
89
+ deletionResults = await processDeletions({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant }, deletions);
83
90
  }
84
91
  // 10. afterUpdate hook (inside the transaction: throwing rolls everything back)
85
92
  if (tableConf.afterUpdate) {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Correlated aggregate subqueries — the machinery behind `<alias>.<fn>.<field>` in both
3
+ * `orderBy` and `conditions` (HAVING-style).
4
+ *
5
+ * Kept separate from ./conditions.ts on purpose: an aggregate embeds a join-reference
6
+ * condition, while a dotted condition embeds an aggregate. Holding both in one module would
7
+ * be fine, but splitting them the other way round (aggregates with orderBy) would make
8
+ * order-by and conditions import each other.
9
+ */
10
+ import { Expression } from 'node-condition-builder';
11
+ import type { QueryClient } from '../../db.js';
12
+ import type { QueryParams } from '../query-params.js';
13
+ import type { DbTables, ITable, JoinGroupRequest, SearchCondition, TenantContext } from '../../../types.js';
14
+ export interface AggFn {
15
+ sql: string;
16
+ distinct: boolean;
17
+ }
18
+ export declare const AGG_FN: Record<string, AggFn>;
19
+ export declare function aggExpr(fn: AggFn, qualifiedCol: string): string;
20
+ /**
21
+ * Correlated aggregate subquery for `<alias>.<fn>.<field>`, as an Expression carrying the
22
+ * values of its optional filter. Markers are `?`, so whoever embeds it — a ConditionBuilder
23
+ * or an ORDER BY list — decides the placeholder positions.
24
+ *
25
+ * The subquery carries the join table's own tenant scope, exactly as `executeJoinGroup` does
26
+ * for the aggregations it returns. Without it, ordering and HAVING-style filtering would count
27
+ * child rows of other tenants, disagreeing with the scoped `result.joinGroup` for the same
28
+ * alias and leaking their existence.
29
+ */
30
+ export declare function buildAggOrderExpr(db: QueryClient, dbTables: DbTables, tableConf: ITable, alias: string, fn: string, field: string, joinGroup: Record<string, JoinGroupRequest> | undefined, tenant?: TenantContext): Expression;
31
+ /** HAVING-style conditions: the dotted `<alias>.<fn>.<field>` entries of `conditions`. */
32
+ export declare function appendAggConditions(params: QueryParams, conditions: SearchCondition[], db: QueryClient, dbTables: DbTables, tableConf: ITable, joinGroup: Record<string, JoinGroupRequest> | undefined, tenant?: TenantContext): string;
33
+ //# sourceMappingURL=aggregations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregations.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/aggregations.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAoB,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAMtD,OAAO,KAAK,EACV,QAAQ,EACR,MAAM,EACN,gBAAgB,EAChB,eAAe,EACf,aAAa,EACd,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,KAAK;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE;AAEzD,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAOxC,CAAC;AAEF,wBAAgB,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAAG,SAAS,EACvD,MAAM,CAAC,EAAE,aAAa,GACrB,UAAU,CAgEZ;AAED,0FAA0F;AAC1F,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,eAAe,EAAE,EAC7B,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAAG,SAAS,EACvD,MAAM,CAAC,EAAE,aAAa,GACrB,MAAM,CAyBR"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Correlated aggregate subqueries — the machinery behind `<alias>.<fn>.<field>` in both
3
+ * `orderBy` and `conditions` (HAVING-style).
4
+ *
5
+ * Kept separate from ./conditions.ts on purpose: an aggregate embeds a join-reference
6
+ * condition, while a dotted condition embeds an aggregate. Holding both in one module would
7
+ * be fine, but splitting them the other way round (aggregates with orderBy) would make
8
+ * order-by and conditions import each other.
9
+ */
10
+ import { ConditionBuilder, Expression } from 'node-condition-builder';
11
+ import { ALLOWED_SET } from '../../condition-methods.js';
12
+ import { tenantForTable, buildTenantRowGuard } from '../../tenant.js';
13
+ import { err400, validateSchemaField } from './fields.js';
14
+ import { requireJoin, extractJoinRefs, subqueryAlias } from './joins.js';
15
+ import { buildJoinRefCondition, dispatchConditionMethod } from './conditions.js';
16
+ export const AGG_FN = {
17
+ sum: { sql: 'SUM', distinct: false },
18
+ min: { sql: 'MIN', distinct: false },
19
+ max: { sql: 'MAX', distinct: false },
20
+ avg: { sql: 'AVG', distinct: false },
21
+ count: { sql: 'COUNT', distinct: false },
22
+ distinctCount: { sql: 'COUNT', distinct: true },
23
+ };
24
+ export function aggExpr(fn, qualifiedCol) {
25
+ return fn.distinct ? `COUNT(DISTINCT ${qualifiedCol})` : `${fn.sql}(${qualifiedCol})`;
26
+ }
27
+ /**
28
+ * Correlated aggregate subquery for `<alias>.<fn>.<field>`, as an Expression carrying the
29
+ * values of its optional filter. Markers are `?`, so whoever embeds it — a ConditionBuilder
30
+ * or an ORDER BY list — decides the placeholder positions.
31
+ *
32
+ * The subquery carries the join table's own tenant scope, exactly as `executeJoinGroup` does
33
+ * for the aggregations it returns. Without it, ordering and HAVING-style filtering would count
34
+ * child rows of other tenants, disagreeing with the scoped `result.joinGroup` for the same
35
+ * alias and leaking their existence.
36
+ */
37
+ export function buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup, tenant) {
38
+ const aggFn = AGG_FN[fn];
39
+ if (!aggFn)
40
+ err400(`Invalid aggregation function: ${fn}`);
41
+ const groupReq = joinGroup?.[alias];
42
+ if (!groupReq)
43
+ err400(`orderBy/conditions reference undeclared joinGroup: ${alias}`);
44
+ const declaredFields = groupReq.aggregations[fn];
45
+ if (!Array.isArray(declaredFields) || !declaredFields.includes(field)) {
46
+ err400(`orderBy/conditions reference undeclared aggregation: ${alias}.${fn}.${field}`);
47
+ }
48
+ const joinDef = requireJoin(tableConf, alias, false);
49
+ const { joinSchema, joinField } = joinDef;
50
+ if (groupReq.aggregations.by && groupReq.aggregations.by !== joinField) {
51
+ err400(`Cannot order by aggregation on joinGroup with 'by' clause on non-FK column: ${alias} (grouped by '${groupReq.aggregations.by}', correlation FK is '${joinField}')`);
52
+ }
53
+ // Pass the join table's conf: without it validateSchemaField skips the readExclude
54
+ // check, and a HAVING-style condition on a hidden field becomes a 400-vs-200 oracle
55
+ // (executeJoinGroup's own validation is skipped when the main result set is empty).
56
+ const fieldCol = validateSchemaField(field, joinSchema, dbTables[joinSchema.tableName]);
57
+ const refs = extractJoinRefs(db, tableConf, joinDef);
58
+ // The subquery FROM is aliased so the correlation to the outer table survives a
59
+ // self-referencing relation; everything inside references the alias.
60
+ const subAlias = subqueryAlias(alias, tableConf);
61
+ const subRef = db.qi(subAlias);
62
+ let filterWhere = '';
63
+ let filterVals = [];
64
+ const joinTableConf = dbTables[joinSchema.tableName];
65
+ // Tenant scope first: it is emitted before the request's own filter, so its values must be
66
+ // bound in that order too.
67
+ const guard = buildTenantRowGuard(db, tenantForTable(tenant, joinTableConf), subAlias);
68
+ let tenantWhere = '';
69
+ const tenantVals = [];
70
+ if (guard) {
71
+ const rendered = guard.toExpression();
72
+ tenantWhere = ` AND ${rendered.value}`;
73
+ tenantVals.push(...rendered.values);
74
+ }
75
+ if (groupReq.filters || groupReq.conditions?.length) {
76
+ const cb = buildJoinRefCondition(joinTableConf, joinSchema, { filters: groupReq.filters, conditions: groupReq.conditions }, db, subAlias);
77
+ const fragment = cb.toExpression();
78
+ if (fragment.value) {
79
+ filterWhere = ` AND ${fragment.value}`;
80
+ filterVals = [...fragment.values];
81
+ }
82
+ }
83
+ const qualifiedCol = `${subRef}.${db.qi(fieldCol)}`;
84
+ const expr = `COALESCE((SELECT ${aggExpr(aggFn, qualifiedCol)} FROM ${refs.joinTable} AS ${subRef} WHERE ${subRef}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}${tenantWhere}${filterWhere}), 0)`;
85
+ return new Expression(expr, [...tenantVals, ...filterVals]);
86
+ }
87
+ /** HAVING-style conditions: the dotted `<alias>.<fn>.<field>` entries of `conditions`. */
88
+ export function appendAggConditions(params, conditions, db, dbTables, tableConf, joinGroup, tenant) {
89
+ let where = '';
90
+ for (const c of conditions) {
91
+ if (!c.field.includes('.'))
92
+ continue;
93
+ if (!ALLOWED_SET.has(c.method)) {
94
+ err400(`Invalid condition method: ${c.method}`);
95
+ }
96
+ const parts = c.field.split('.');
97
+ if (parts.length !== 3) {
98
+ err400(`Invalid dotted field in condition: ${c.field} (expected <alias>.<fn>.<field>)`);
99
+ }
100
+ const [alias, fn, field] = parts;
101
+ // The aggregate carries the values of its own filter; passing it as the left-hand side
102
+ // lets the ConditionBuilder place those and the compared value in one step.
103
+ const expr = buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup, tenant);
104
+ const tmpCb = new ConditionBuilder('AND', db.cbDialect);
105
+ dispatchConditionMethod(tmpCb, c.method, expr, c.params);
106
+ where += ` AND ${params.emitCondition(tmpCb, db)}`;
107
+ }
108
+ return where;
109
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Turning a request's `filters` and `conditions` into ConditionBuilder clauses.
3
+ *
4
+ * The method vocabulary itself lives in lib/condition-methods.ts, which is a pure data
5
+ * module shared with the TypeBox schema builders — the dispatch below is kept out of it so
6
+ * that module keeps its zero dependencies.
7
+ */
8
+ import { ConditionBuilder, Expression } from 'node-condition-builder';
9
+ import type { QueryClient } from '../../db.js';
10
+ import type { FilterRecord, ITable, JoinRefFilter, SchemaDefinition, SearchCondition } from '../../../types.js';
11
+ /**
12
+ * Apply one condition method to a builder, after checking the request actually supplied the
13
+ * operands that method needs.
14
+ *
15
+ * `params` is optional in the request schema, so `{field, method}` with no params is a
16
+ * well-formed request that reaches here — reading `params[0]` off it throws a TypeError the
17
+ * error handler can only report as a 500. Arity is checked here rather than at the four call
18
+ * sites so no path can skip it.
19
+ */
20
+ export declare function dispatchConditionMethod(cb: ConditionBuilder, method: string, colOrExpr: string | Expression, params: unknown[] | undefined): void;
21
+ export declare function buildJoinRefCondition(joinTableConf: ITable | undefined, joinSchema: SchemaDefinition, ref: JoinRefFilter, db: QueryClient, qualifier?: string): ConditionBuilder;
22
+ /**
23
+ * Apply non-dotted conditions to the main `condition` ConditionBuilder when
24
+ * possible. For schema fields → straight dispatch. For computed fields → return
25
+ * side-channel clauses to be appended to WHERE later (with correct placeholder
26
+ * offsets), since ConditionBuilder cannot bind values for an LHS expression.
27
+ */
28
+ export declare function applyConditions(condition: ConditionBuilder, conditions: SearchCondition[], schema: SchemaDefinition, tableConf: ITable, db: QueryClient): void;
29
+ /** Computed-field equality filters on the main table. */
30
+ export declare function applyComputedFilters(condition: ConditionBuilder, filters: FilterRecord, tableConf: ITable, db: QueryClient): void;
31
+ //# sourceMappingURL=conditions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conditions.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/conditions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAW/C,OAAO,KAAK,EAEV,YAAY,EACZ,MAAM,EACN,aAAa,EACb,gBAAgB,EAChB,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,gBAAgB,EACpB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAAG,UAAU,EAC9B,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,GAC5B,IAAI,CA4BN;AAuDD,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,UAAU,EAAE,gBAAgB,EAC5B,GAAG,EAAE,aAAa,EAClB,EAAE,EAAE,WAAW,EACf,SAAS,CAAC,EAAE,MAAM,GACjB,gBAAgB,CAuBlB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,gBAAgB,EAC3B,UAAU,EAAE,eAAe,EAAE,EAC7B,MAAM,EAAE,gBAAgB,EACxB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,WAAW,GACd,IAAI,CAcN;AAED,yDAAyD;AACzD,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE,YAAY,EACrB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,WAAW,GACd,IAAI,CAIN"}