fastify-auto-sqlapi 0.1.9 → 0.1.11
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.md +3 -1
- package/AGENTS_BACKEND.md +102 -3
- package/AGENTS_FRONTEND.md +69 -545
- package/BREAKING_CHANGES.md +63 -0
- package/CHANGELOG.md +100 -0
- package/README.md +190 -25
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/lib/agent/manifest.d.ts +53 -0
- package/dist/lib/agent/manifest.d.ts.map +1 -0
- package/dist/lib/agent/manifest.js +153 -0
- package/dist/lib/agent/tool-schemas.d.ts +36 -0
- package/dist/lib/agent/tool-schemas.d.ts.map +1 -0
- package/dist/lib/agent/tool-schemas.js +47 -0
- package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
- package/dist/lib/cli/schema-codegen.js +9 -1
- package/dist/lib/engine/bulk/bulk-delete.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-delete.js +5 -1
- package/dist/lib/engine/rest/delete.d.ts.map +1 -1
- package/dist/lib/engine/rest/delete.js +4 -1
- package/dist/lib/engine/rest/get.d.ts.map +1 -1
- package/dist/lib/engine/rest/get.js +4 -1
- package/dist/lib/engine/write-helpers.d.ts +18 -4
- package/dist/lib/engine/write-helpers.d.ts.map +1 -1
- package/dist/lib/engine/write-helpers.js +31 -10
- package/dist/lib/nullable.d.ts +17 -0
- package/dist/lib/nullable.d.ts.map +1 -0
- package/dist/lib/nullable.js +24 -0
- package/dist/lib/schema/search.d.ts +11 -9
- package/dist/lib/schema/search.d.ts.map +1 -1
- package/dist/lib/schema/search.js +27 -10
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +12 -2
- package/dist/routes/auto/agent-manifest.routes.d.ts +12 -0
- package/dist/routes/auto/agent-manifest.routes.d.ts.map +1 -0
- package/dist/routes/auto/agent-manifest.routes.js +30 -0
- package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-delete.routes.js +1 -0
- 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/plugin.d.ts.map +1 -1
- package/dist/routes/auto/plugin.js +4 -0
- package/dist/routes/auto/route-helpers.d.ts +7 -0
- package/dist/routes/auto/route-helpers.d.ts.map +1 -1
- package/dist/routes/auto/route-helpers.js +11 -0
- package/dist/routes/auto/search.routes.js +3 -3
- package/dist/types/plugin.d.ts +7 -0
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/table.d.ts +6 -0
- package/dist/types/table.d.ts.map +1 -1
- package/dist/types/table.js +8 -0
- package/docs/adr/0001-no-orm-raw-sql.md +38 -0
- package/docs/adr/0002-open-by-default.md +37 -0
- package/docs/adr/0003-bulk-not-transactional.md +35 -0
- package/docs/adr/0004-updates-always-open.md +46 -0
- package/docs/adr/0005-insert-pipeline-sanitize-before-hooks.md +51 -0
- package/docs/adr/0006-raw-db-errors.md +34 -0
- package/docs/adr/0007-explicit-table-generation.md +39 -0
- package/docs/adr/README.md +50 -0
- package/package.json +2 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { isCompositePrimary } from '../../types.js';
|
|
2
|
+
const ALL_OPERATIONS = [
|
|
3
|
+
'search', 'get', 'insert', 'update', 'delete', 'bulkUpsert', 'bulkDelete',
|
|
4
|
+
];
|
|
5
|
+
const SINGLE_PK_OPERATIONS = new Set(['get', 'delete', 'bulkDelete']);
|
|
6
|
+
function shortType(schema) {
|
|
7
|
+
if (!schema)
|
|
8
|
+
return 'any';
|
|
9
|
+
const t = schema.type;
|
|
10
|
+
if (!t)
|
|
11
|
+
return 'any';
|
|
12
|
+
const types = (Array.isArray(t) ? t : [t]).filter((x) => x !== 'null');
|
|
13
|
+
return types[0] ?? 'any';
|
|
14
|
+
}
|
|
15
|
+
function isNullable(schema) {
|
|
16
|
+
const t = schema?.type;
|
|
17
|
+
return Array.isArray(t) && t.includes('null');
|
|
18
|
+
}
|
|
19
|
+
function computedTypes(tableConf) {
|
|
20
|
+
if (!tableConf.computedFields)
|
|
21
|
+
return undefined;
|
|
22
|
+
const out = {};
|
|
23
|
+
for (const [name, fn] of Object.entries(tableConf.computedFields)) {
|
|
24
|
+
try {
|
|
25
|
+
const stub = fn({
|
|
26
|
+
db: { qi: (s) => s, dialectName: 'postgres' },
|
|
27
|
+
qiCol: () => '""',
|
|
28
|
+
});
|
|
29
|
+
out[name] = shortType(stub.type);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
out[name] = 'any';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return Object.keys(out).length ? out : undefined;
|
|
36
|
+
}
|
|
37
|
+
export function tableOperations(tableConf) {
|
|
38
|
+
const requested = tableConf.operations ?? ALL_OPERATIONS;
|
|
39
|
+
if (!isCompositePrimary(tableConf.primary))
|
|
40
|
+
return [...requested];
|
|
41
|
+
return requested.filter((op) => !SINGLE_PK_OPERATIONS.has(op));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build the machine-readable description of every table exposed by this deployment:
|
|
45
|
+
* fields (type/required/nullable), enabled operations, join aliases, computed fields,
|
|
46
|
+
* extra filters, tenant scoping. Derived entirely from `DbTables`, so it is always in
|
|
47
|
+
* sync with the running configuration. Pairs with AGENTS_FRONTEND.md (the request grammar)
|
|
48
|
+
* to give an LLM client everything it needs to drive the API.
|
|
49
|
+
*/
|
|
50
|
+
export function buildAgentManifest(dbTables) {
|
|
51
|
+
const tables = {};
|
|
52
|
+
for (const [key, tableConf] of Object.entries(dbTables)) {
|
|
53
|
+
const schema = tableConf.Schema;
|
|
54
|
+
const required = new Set((schema.validation.required) ?? []);
|
|
55
|
+
const readExcluded = new Set(tableConf.readExclude ?? []);
|
|
56
|
+
const fields = {};
|
|
57
|
+
for (const [name, fieldSchema] of Object.entries(schema.fields)) {
|
|
58
|
+
const f = { type: shortType(fieldSchema) };
|
|
59
|
+
if (required.has(name))
|
|
60
|
+
f.required = true;
|
|
61
|
+
if (isNullable(fieldSchema))
|
|
62
|
+
f.nullable = true;
|
|
63
|
+
if (readExcluded.has(name))
|
|
64
|
+
f.writeOnly = true;
|
|
65
|
+
fields[name] = f;
|
|
66
|
+
}
|
|
67
|
+
const readJoins = (tableConf.allowedReadJoins ?? []).map((j) => ({
|
|
68
|
+
alias: j.alias,
|
|
69
|
+
table: j.joinSchema.tableName,
|
|
70
|
+
kind: j.unique ? 'N:1' : '1:N',
|
|
71
|
+
}));
|
|
72
|
+
const writeJoins = (tableConf.allowedWriteJoins ?? []).map((j) => ({
|
|
73
|
+
alias: j.alias,
|
|
74
|
+
table: j.joinSchema.tableName,
|
|
75
|
+
kind: j.unique ? 'N:1' : '1:N',
|
|
76
|
+
}));
|
|
77
|
+
const extraFilters = Object.keys(tableConf.extraFilters ?? {}).length
|
|
78
|
+
? Object.fromEntries(Object.entries(tableConf.extraFilters).map(([n, s]) => [n, shortType(s)]))
|
|
79
|
+
: undefined;
|
|
80
|
+
const entry = {
|
|
81
|
+
table: schema.tableName,
|
|
82
|
+
primary: tableConf.primary,
|
|
83
|
+
operations: tableOperations(tableConf),
|
|
84
|
+
fields,
|
|
85
|
+
};
|
|
86
|
+
if (tableConf.excludeFromCreation?.length)
|
|
87
|
+
entry.serverGenerated = [...tableConf.excludeFromCreation];
|
|
88
|
+
const computed = computedTypes(tableConf);
|
|
89
|
+
if (computed)
|
|
90
|
+
entry.computed = computed;
|
|
91
|
+
if (extraFilters)
|
|
92
|
+
entry.extraFilters = extraFilters;
|
|
93
|
+
if (readJoins.length)
|
|
94
|
+
entry.readJoins = readJoins;
|
|
95
|
+
if (writeJoins.length)
|
|
96
|
+
entry.writeJoins = writeJoins;
|
|
97
|
+
if (tableConf.tenantScope)
|
|
98
|
+
entry.tenantScoped = true;
|
|
99
|
+
if (tableConf.defaultOrder)
|
|
100
|
+
entry.defaultOrder = tableConf.defaultOrder;
|
|
101
|
+
tables[key] = entry;
|
|
102
|
+
}
|
|
103
|
+
return { tables };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Compact markdown rendering of the manifest, designed for an LLM system prompt.
|
|
107
|
+
* Notation: `!` required on insert, `?` nullable, `(writeOnly)` never returned by reads.
|
|
108
|
+
*/
|
|
109
|
+
export function renderAgentManifestMd(manifest) {
|
|
110
|
+
const lines = [
|
|
111
|
+
'# Tables',
|
|
112
|
+
'',
|
|
113
|
+
'Notation: `!`=required on insert, `?`=nullable, `(writeOnly)`=writable, never in reads.',
|
|
114
|
+
'serverGenerated fields: omit on insert (values from client are ignored).',
|
|
115
|
+
'readJoins aliases: `1:N` → joinMustExist/joinMultiple/joinGroup; `N:1` → joinLeft.',
|
|
116
|
+
'writeJoins aliases: keys for `secondaries`/`deletions` in writes.',
|
|
117
|
+
'',
|
|
118
|
+
];
|
|
119
|
+
for (const t of Object.values(manifest.tables)) {
|
|
120
|
+
const pk = Array.isArray(t.primary) ? t.primary.join('+') : t.primary;
|
|
121
|
+
const flags = t.tenantScoped ? ' [tenant-scoped]' : '';
|
|
122
|
+
lines.push(`## ${t.table} PK:${pk} ops:${t.operations.join(',')}${flags}`);
|
|
123
|
+
const fieldParts = Object.entries(t.fields).map(([name, f]) => {
|
|
124
|
+
let s = `${name}:${f.type}`;
|
|
125
|
+
if (f.required)
|
|
126
|
+
s += '!';
|
|
127
|
+
if (f.nullable)
|
|
128
|
+
s += '?';
|
|
129
|
+
if (f.writeOnly)
|
|
130
|
+
s += '(writeOnly)';
|
|
131
|
+
return s;
|
|
132
|
+
});
|
|
133
|
+
lines.push(`fields: ${fieldParts.join(', ')}`);
|
|
134
|
+
if (t.serverGenerated)
|
|
135
|
+
lines.push(`serverGenerated: ${t.serverGenerated.join(', ')}`);
|
|
136
|
+
if (t.computed) {
|
|
137
|
+
lines.push(`computed: ${Object.entries(t.computed).map(([n, ty]) => `${n}:${ty}`).join(', ')}`);
|
|
138
|
+
}
|
|
139
|
+
if (t.extraFilters) {
|
|
140
|
+
lines.push(`extraFilters: ${Object.entries(t.extraFilters).map(([n, ty]) => `${n}:${ty}`).join(', ')}`);
|
|
141
|
+
}
|
|
142
|
+
if (t.readJoins) {
|
|
143
|
+
lines.push(`readJoins: ${t.readJoins.map((j) => `${j.alias}→${j.table}(${j.kind})`).join(', ')}`);
|
|
144
|
+
}
|
|
145
|
+
if (t.writeJoins) {
|
|
146
|
+
lines.push(`writeJoins: ${t.writeJoins.map((j) => `${j.alias}→${j.table}`).join(', ')}`);
|
|
147
|
+
}
|
|
148
|
+
if (t.defaultOrder)
|
|
149
|
+
lines.push(`defaultOrder: ${t.defaultOrder}`);
|
|
150
|
+
lines.push('');
|
|
151
|
+
}
|
|
152
|
+
return lines.join('\n');
|
|
153
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DbTables } from '../../types.js';
|
|
2
|
+
export interface AgentToolSchemas {
|
|
3
|
+
/** POST /search/{table}: `body` + `querystring` (orderBy/page/itemsPerPage/compute*). */
|
|
4
|
+
search?: {
|
|
5
|
+
body: object;
|
|
6
|
+
querystring: object;
|
|
7
|
+
};
|
|
8
|
+
insert?: {
|
|
9
|
+
body: object;
|
|
10
|
+
};
|
|
11
|
+
update?: {
|
|
12
|
+
body: object;
|
|
13
|
+
};
|
|
14
|
+
bulkUpsert?: {
|
|
15
|
+
body: object;
|
|
16
|
+
};
|
|
17
|
+
bulkDelete?: {
|
|
18
|
+
body: object;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Ready-to-use JSON Schemas for exposing a table's operations as strict LLM tools —
|
|
23
|
+
* the same TypeBox schemas the HTTP routes validate with (TypeBox objects ARE plain
|
|
24
|
+
* JSON Schema). Only the operations actually enabled for the table are included
|
|
25
|
+
* (`operations` whitelist, composite-PK rules). `get` and `delete` take a single PK
|
|
26
|
+
* path param and need no schema.
|
|
27
|
+
*
|
|
28
|
+
* Strict tools trade prompt size for provider-side validation; the lighter
|
|
29
|
+
* alternative is a loose tool plus the plugin's structured 400 as the retry signal
|
|
30
|
+
* (see "LLM / agent clients" in the README).
|
|
31
|
+
*/
|
|
32
|
+
export declare function agentToolSchemas(dbTables: DbTables, tableName: string, opts?: {
|
|
33
|
+
maxItemsPerPage?: number;
|
|
34
|
+
maxBulkItems?: number;
|
|
35
|
+
}): AgentToolSchemas;
|
|
36
|
+
//# sourceMappingURL=tool-schemas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-schemas.d.ts","sourceRoot":"","sources":["../../../src/lib/agent/tool-schemas.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,MAAM,WAAW,gBAAgB;IAC/B,yFAAyF;IACzF,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1B,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1B,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GACzD,gBAAgB,CA6BlB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { SearchTableBodyPost, SearchTableQuery } from '../schema/search.js';
|
|
2
|
+
import { InsertTableBody } from '../schema/insert.js';
|
|
3
|
+
import { UpdateTableBody } from '../schema/update.js';
|
|
4
|
+
import { BulkUpsertTableBody } from '../schema/bulk-upsert.js';
|
|
5
|
+
import { BulkDeleteTableBody } from '../schema/bulk-delete.js';
|
|
6
|
+
import { tableOperations } from './manifest.js';
|
|
7
|
+
import { DEFAULT_MAX_ITEMS_PER_PAGE, DEFAULT_MAX_BULK_ITEMS } from '../../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Ready-to-use JSON Schemas for exposing a table's operations as strict LLM tools —
|
|
10
|
+
* the same TypeBox schemas the HTTP routes validate with (TypeBox objects ARE plain
|
|
11
|
+
* JSON Schema). Only the operations actually enabled for the table are included
|
|
12
|
+
* (`operations` whitelist, composite-PK rules). `get` and `delete` take a single PK
|
|
13
|
+
* path param and need no schema.
|
|
14
|
+
*
|
|
15
|
+
* Strict tools trade prompt size for provider-side validation; the lighter
|
|
16
|
+
* alternative is a loose tool plus the plugin's structured 400 as the retry signal
|
|
17
|
+
* (see "LLM / agent clients" in the README).
|
|
18
|
+
*/
|
|
19
|
+
export function agentToolSchemas(dbTables, tableName, opts) {
|
|
20
|
+
const tableConf = dbTables[tableName];
|
|
21
|
+
if (!tableConf) {
|
|
22
|
+
throw new Error(`Unknown table "${tableName}". Available: ${Object.keys(dbTables).join(', ')}`);
|
|
23
|
+
}
|
|
24
|
+
const ops = new Set(tableOperations(tableConf));
|
|
25
|
+
const out = {};
|
|
26
|
+
if (ops.has('search')) {
|
|
27
|
+
out.search = {
|
|
28
|
+
body: SearchTableBodyPost(dbTables, tableName),
|
|
29
|
+
querystring: SearchTableQuery(opts?.maxItemsPerPage ?? DEFAULT_MAX_ITEMS_PER_PAGE),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (ops.has('insert'))
|
|
33
|
+
out.insert = { body: InsertTableBody(dbTables, tableName) };
|
|
34
|
+
if (ops.has('update'))
|
|
35
|
+
out.update = { body: UpdateTableBody(dbTables, tableName) };
|
|
36
|
+
if (ops.has('bulkUpsert')) {
|
|
37
|
+
out.bulkUpsert = {
|
|
38
|
+
body: BulkUpsertTableBody(dbTables, tableName, opts?.maxBulkItems ?? DEFAULT_MAX_BULK_ITEMS),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (ops.has('bulkDelete')) {
|
|
42
|
+
out.bulkDelete = {
|
|
43
|
+
body: BulkDeleteTableBody(dbTables, tableName, opts?.maxBulkItems ?? DEFAULT_MAX_BULK_ITEMS),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/schema-codegen.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE3D,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE;IAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9F,MAAM,
|
|
1
|
+
{"version":3,"file":"schema-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/schema-codegen.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE3D,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE;IAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9F,MAAM,CA0DR;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,QAAQ,CAqC1D;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,CAAC,EAAE,MAAM,EAAE,GACjB,MAAM,CA8CR"}
|
|
@@ -4,6 +4,12 @@ export function convertColType(udtName, columnInfo) {
|
|
|
4
4
|
const hasDefault = columnInfo.column_default || columnInfo.is_auto_increment;
|
|
5
5
|
const isNullable = columnInfo.is_nullable === 'YES';
|
|
6
6
|
const wrap = (t) => {
|
|
7
|
+
// Nullable columns need the type-array form (`Nullable(T)` → type: [..., 'null']):
|
|
8
|
+
// Type.Optional alone means "key may be absent", and serializing a NULL value against
|
|
9
|
+
// a plain type coerces it (0 / "" / false). A Type.Union with Null is no better —
|
|
10
|
+
// Ajv's coerceTypes corrupts null/0/"" through the union branches.
|
|
11
|
+
if (isNullable)
|
|
12
|
+
t = `Nullable(${t})`;
|
|
7
13
|
return hasDefault || isNullable ? `Type.Optional(${t})` : t;
|
|
8
14
|
};
|
|
9
15
|
switch (udtName) {
|
|
@@ -83,8 +89,10 @@ export function generateSchemaFile(schemaName, tableName, fields, colMap, primar
|
|
|
83
89
|
const fieldsStr = JSON.stringify(fields, null, 2).replace(/"(.*?)"\s*:\s*"Type(.*?)"/g, '$1: Type$2');
|
|
84
90
|
const colMapStr = JSON.stringify(colMap, null, 2);
|
|
85
91
|
const primaryKeyLine = primary?.length ? `\n primaryKey: ${JSON.stringify(primary)},` : '';
|
|
92
|
+
const usesNullable = Object.values(fields).some((t) => t.includes('Nullable('));
|
|
93
|
+
const valueImports = usesNullable ? 'Type, Nullable' : 'Type';
|
|
86
94
|
return `// Auto-generated by fastify-auto-sqlapi. Do not edit.
|
|
87
|
-
import {
|
|
95
|
+
import { ${valueImports} } from "fastify-auto-sqlapi";
|
|
88
96
|
import type { Static } from "fastify-auto-sqlapi";
|
|
89
97
|
|
|
90
98
|
const _Schema = ${fieldsStr};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulk-delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-delete.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bulk-delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-delete.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE5E,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAuE5F"}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { ConditionBuilder } from 'node-condition-builder';
|
|
2
2
|
import { buildTenantDeleteWhere, assertTenantOwnsAll } from '../../tenant.js';
|
|
3
|
-
import { primaryAsString } from '../../../types.js';
|
|
3
|
+
import { primaryAsString, isCompositePrimary } from '../../../types.js';
|
|
4
|
+
import { httpError } from '../../errors.js';
|
|
4
5
|
export async function bulkDeleteEngine(params) {
|
|
5
6
|
const { db, tableConf, ids, tenant, request } = params;
|
|
7
|
+
if (isCompositePrimary(tableConf.primary)) {
|
|
8
|
+
throw httpError(400, `Table "${tableConf.Schema.tableName}" has a composite primary key: bulk delete matches on "${primaryAsString(tableConf.primary)}" alone and could remove multiple rows per id. Use a custom query.`);
|
|
9
|
+
}
|
|
6
10
|
if (!ids.length)
|
|
7
11
|
return [];
|
|
8
12
|
const pk = primaryAsString(tableConf.primary);
|
|
@@ -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,CAuC9E"}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { buildTenantDeleteWhere, assertTenantOwnsAll } from '../../tenant.js';
|
|
2
2
|
import { httpError } from '../../errors.js';
|
|
3
|
-
import { primaryAsString } from '../../../types.js';
|
|
3
|
+
import { primaryAsString, isCompositePrimary } from '../../../types.js';
|
|
4
4
|
export async function deleteEngine(params) {
|
|
5
5
|
const { db, tableConf, id, tenant, request } = params;
|
|
6
|
+
if (isCompositePrimary(tableConf.primary)) {
|
|
7
|
+
throw httpError(400, `Table "${tableConf.Schema.tableName}" has a composite primary key: delete-by-id would match on "${primaryAsString(tableConf.primary)}" alone and could remove multiple rows. Use a custom query.`);
|
|
8
|
+
}
|
|
6
9
|
const pk = primaryAsString(tableConf.primary);
|
|
7
10
|
const pkCol = tableConf.Schema.col(pk);
|
|
8
11
|
const tableName = tableConf.Schema.tableName;
|
|
@@ -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,
|
|
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"}
|
|
@@ -3,9 +3,12 @@ import { camelcaseObject } from '../../naming.js';
|
|
|
3
3
|
import { buildTenantCondition, buildTenantJoin } from '../../tenant.js';
|
|
4
4
|
import { httpError } from '../../errors.js';
|
|
5
5
|
import { readableSelectColumns } from '../../read-access.js';
|
|
6
|
-
import { primaryAsString } from '../../../types.js';
|
|
6
|
+
import { primaryAsString, isCompositePrimary } from '../../../types.js';
|
|
7
7
|
export async function getEngine(params) {
|
|
8
8
|
const { db, tableConf, id, tenant } = params;
|
|
9
|
+
if (isCompositePrimary(tableConf.primary)) {
|
|
10
|
+
throw httpError(400, `Table "${tableConf.Schema.tableName}" has a composite primary key: get-by-id would match on "${primaryAsString(tableConf.primary)}" alone. Use search with all PK fields.`);
|
|
11
|
+
}
|
|
9
12
|
const pkCol = tableConf.Schema.col(primaryAsString(tableConf.primary));
|
|
10
13
|
const cb = new ConditionBuilder('AND', db.cbDialect);
|
|
11
14
|
// Table-qualified: the tenant scope may add a through-join, and a bare PK column
|
|
@@ -2,7 +2,20 @@ import type { FastifyRequest } from 'fastify';
|
|
|
2
2
|
import type { QueryClient } from '../db.js';
|
|
3
3
|
import type { ITable, DbTables, JoinDefinition } from '../../types.js';
|
|
4
4
|
export declare function findWriteJoin(tableConf: ITable, alias: string): JoinDefinition | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Drop `excludeFromCreation` columns from a snake_case (DB-format) record.
|
|
7
|
+
*
|
|
8
|
+
* Exclusion is a whitelist on CLIENT input: call this on the client payload only,
|
|
9
|
+
* BEFORE any server-side value is assigned (beforeInsert mutations, FK auto-fill),
|
|
10
|
+
* so engine/hook-generated values on those fields reach the SQL.
|
|
11
|
+
*/
|
|
5
12
|
export declare function removeExcludedFields(record: Record<string, unknown>, tableConf: ITable): Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Same whitelist as `removeExcludedFields`, but for a camelCase record (schema
|
|
15
|
+
* field names) and in place. Used on the client payload before the beforeInsert
|
|
16
|
+
* hook runs.
|
|
17
|
+
*/
|
|
18
|
+
export declare function removeExcludedFieldsCamel(record: Record<string, unknown>, tableConf: ITable): void;
|
|
6
19
|
export declare function findSecondaryTableConf(dbTables: DbTables, joinTableName: string): ITable | undefined;
|
|
7
20
|
interface PrepareCtx {
|
|
8
21
|
db: QueryClient;
|
|
@@ -12,10 +25,11 @@ interface PrepareCtx {
|
|
|
12
25
|
/**
|
|
13
26
|
* Prepare a single record for INSERT/UPSERT:
|
|
14
27
|
* 1. shallow camelCase copy of `input`
|
|
15
|
-
* 2. runValidation (camelCase)
|
|
16
|
-
* 3.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
28
|
+
* 2. runValidation (camelCase — sees the client payload as sent)
|
|
29
|
+
* 3. drop excludeFromCreation fields (client-input whitelist; applied BEFORE the
|
|
30
|
+
* hook so values it assigns — e.g. a server-generated id — reach the INSERT)
|
|
31
|
+
* 4. beforeInsert hook (caller can mutate the camelCase copy)
|
|
32
|
+
* 5. snakecaseRecord → DB format
|
|
19
33
|
*
|
|
20
34
|
* Returns both `camel` (input + hook mutations, used for FK auto-fill / afterInsert) and
|
|
21
35
|
* `snake` (DB-ready record). The original `input` is left intact.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write-helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/engine/write-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EACV,MAAM,EACN,QAAQ,EACR,cAAc,EAEf,MAAM,gBAAgB,CAAC;AAExB,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,cAAc,GAAG,SAAS,CAE5B;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,GAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQzB;AAED,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,MAAM,GACpB,MAAM,GAAG,SAAS,CAKpB;AAED,UAAU,UAAU;IAClB,EAAE,EAAE,WAAW,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,cAAc,CAAC;CACzB;AAED
|
|
1
|
+
{"version":3,"file":"write-helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/engine/write-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EACV,MAAM,EACN,QAAQ,EACR,cAAc,EAEf,MAAM,gBAAgB,CAAC;AAExB,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,cAAc,GAAG,SAAS,CAE5B;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,GAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQzB;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,GAChB,IAAI,CAKN;AAED,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,MAAM,GACpB,MAAM,GAAG,SAAS,CAKpB;AAED,UAAU,UAAU;IAClB,EAAE,EAAE,WAAW,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,cAAc,CAAC;CACzB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,EACvD,OAAO,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,GACnC,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAe7E;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,WAAW,EACf,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAClB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GACrD,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CA4DpD;AAED;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,WAAW,EACf,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GACnD,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CA+BpD"}
|
|
@@ -3,6 +3,13 @@ import { runValidation } from './validate.js';
|
|
|
3
3
|
export function findWriteJoin(tableConf, alias) {
|
|
4
4
|
return tableConf.allowedWriteJoins?.find((j) => j.alias === alias);
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Drop `excludeFromCreation` columns from a snake_case (DB-format) record.
|
|
8
|
+
*
|
|
9
|
+
* Exclusion is a whitelist on CLIENT input: call this on the client payload only,
|
|
10
|
+
* BEFORE any server-side value is assigned (beforeInsert mutations, FK auto-fill),
|
|
11
|
+
* so engine/hook-generated values on those fields reach the SQL.
|
|
12
|
+
*/
|
|
6
13
|
export function removeExcludedFields(record, tableConf) {
|
|
7
14
|
if (!tableConf.excludeFromCreation?.length)
|
|
8
15
|
return record;
|
|
@@ -13,6 +20,18 @@ export function removeExcludedFields(record, tableConf) {
|
|
|
13
20
|
}
|
|
14
21
|
return result;
|
|
15
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Same whitelist as `removeExcludedFields`, but for a camelCase record (schema
|
|
25
|
+
* field names) and in place. Used on the client payload before the beforeInsert
|
|
26
|
+
* hook runs.
|
|
27
|
+
*/
|
|
28
|
+
export function removeExcludedFieldsCamel(record, tableConf) {
|
|
29
|
+
if (!tableConf.excludeFromCreation?.length)
|
|
30
|
+
return;
|
|
31
|
+
for (const field of tableConf.excludeFromCreation) {
|
|
32
|
+
delete record[field];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
16
35
|
export function findSecondaryTableConf(dbTables, joinTableName) {
|
|
17
36
|
for (const [, conf] of Object.entries(dbTables)) {
|
|
18
37
|
if (conf.Schema.tableName === joinTableName)
|
|
@@ -23,10 +42,11 @@ export function findSecondaryTableConf(dbTables, joinTableName) {
|
|
|
23
42
|
/**
|
|
24
43
|
* Prepare a single record for INSERT/UPSERT:
|
|
25
44
|
* 1. shallow camelCase copy of `input`
|
|
26
|
-
* 2. runValidation (camelCase)
|
|
27
|
-
* 3.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
45
|
+
* 2. runValidation (camelCase — sees the client payload as sent)
|
|
46
|
+
* 3. drop excludeFromCreation fields (client-input whitelist; applied BEFORE the
|
|
47
|
+
* hook so values it assigns — e.g. a server-generated id — reach the INSERT)
|
|
48
|
+
* 4. beforeInsert hook (caller can mutate the camelCase copy)
|
|
49
|
+
* 5. snakecaseRecord → DB format
|
|
30
50
|
*
|
|
31
51
|
* Returns both `camel` (input + hook mutations, used for FK auto-fill / afterInsert) and
|
|
32
52
|
* `snake` (DB-ready record). The original `input` is left intact.
|
|
@@ -39,11 +59,11 @@ export async function prepareInsertRecord(ctx, input, secondaries, options) {
|
|
|
39
59
|
if (!options?.skipValidate) {
|
|
40
60
|
await runValidation(ctx.db, ctx.request, ctx.tableConf, camel, secondaries);
|
|
41
61
|
}
|
|
62
|
+
removeExcludedFieldsCamel(camel, ctx.tableConf);
|
|
42
63
|
if (ctx.tableConf.beforeInsert) {
|
|
43
64
|
await ctx.tableConf.beforeInsert(ctx.db, ctx.request, camel);
|
|
44
65
|
}
|
|
45
|
-
|
|
46
|
-
snake = removeExcludedFields(snake, ctx.tableConf);
|
|
66
|
+
const snake = snakecaseRecord(camel, ctx.tableConf.Schema);
|
|
47
67
|
return { camel, snake };
|
|
48
68
|
}
|
|
49
69
|
export async function processSecondaries(db, tableConf, dbTables, mainRecord, secondaries) {
|
|
@@ -63,15 +83,16 @@ export async function processSecondaries(db, tableConf, dbTables, mainRecord, se
|
|
|
63
83
|
: joinSchema.col(secondaryPk);
|
|
64
84
|
const preparedRecords = records.map((rec) => {
|
|
65
85
|
let prepared = snakecaseRecord(rec, joinSchema);
|
|
86
|
+
// Remove excluded fields from the client payload BEFORE the FK auto-fill:
|
|
87
|
+
// an excluded FK column must not strip the engine-injected value below.
|
|
88
|
+
if (secondaryTableConf) {
|
|
89
|
+
prepared = removeExcludedFields(prepared, secondaryTableConf);
|
|
90
|
+
}
|
|
66
91
|
// Auto-fill FK from main record
|
|
67
92
|
const mainValue = Array.isArray(mainField)
|
|
68
93
|
? mainRecord[mainField[0]]
|
|
69
94
|
: mainRecord[mainField];
|
|
70
95
|
prepared[joinCol] = mainValue;
|
|
71
|
-
// Remove excluded fields
|
|
72
|
-
if (secondaryTableConf) {
|
|
73
|
-
prepared = removeExcludedFields(prepared, secondaryTableConf);
|
|
74
|
-
}
|
|
75
96
|
return prepared;
|
|
76
97
|
});
|
|
77
98
|
const secondaryUpsertKeys = tableConf.upsertMap?.get(joinSchema);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Static, type TSchema, type TUnsafe } from '@sinclair/typebox';
|
|
2
|
+
/**
|
|
3
|
+
* Mark a schema as nullable using the JSON-Schema type-array form
|
|
4
|
+
* (`{ type: ['integer', 'null'] }`).
|
|
5
|
+
*
|
|
6
|
+
* This is the only representation that behaves correctly under Fastify's default Ajv
|
|
7
|
+
* `coerceTypes`: with `Type.Union([T, Type.Null()])` a null body value is coerced by the
|
|
8
|
+
* first branch to 0/""/false — and with the Null branch first, a legitimate 0/""/false is
|
|
9
|
+
* coerced to null. With a type array no coercion happens when the value already matches
|
|
10
|
+
* one of the listed types. fast-json-stringify also serializes NULL correctly against a
|
|
11
|
+
* type array, while a bare `Type.Optional(T)` coerces NULL to the type's zero value.
|
|
12
|
+
*
|
|
13
|
+
* Schemas without a `type` keyword (e.g. `Type.Any()`) already admit null and are
|
|
14
|
+
* returned unchanged.
|
|
15
|
+
*/
|
|
16
|
+
export declare function Nullable<T extends TSchema>(schema: T): TUnsafe<Static<T> | null>;
|
|
17
|
+
//# sourceMappingURL=nullable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nullable.d.ts","sourceRoot":"","sources":["../../src/lib/nullable.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,MAAM,EAAE,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAElF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,OAAO,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAMhF"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
/**
|
|
3
|
+
* Mark a schema as nullable using the JSON-Schema type-array form
|
|
4
|
+
* (`{ type: ['integer', 'null'] }`).
|
|
5
|
+
*
|
|
6
|
+
* This is the only representation that behaves correctly under Fastify's default Ajv
|
|
7
|
+
* `coerceTypes`: with `Type.Union([T, Type.Null()])` a null body value is coerced by the
|
|
8
|
+
* first branch to 0/""/false — and with the Null branch first, a legitimate 0/""/false is
|
|
9
|
+
* coerced to null. With a type array no coercion happens when the value already matches
|
|
10
|
+
* one of the listed types. fast-json-stringify also serializes NULL correctly against a
|
|
11
|
+
* type array, while a bare `Type.Optional(T)` coerces NULL to the type's zero value.
|
|
12
|
+
*
|
|
13
|
+
* Schemas without a `type` keyword (e.g. `Type.Any()`) already admit null and are
|
|
14
|
+
* returned unchanged.
|
|
15
|
+
*/
|
|
16
|
+
export function Nullable(schema) {
|
|
17
|
+
const type = schema.type;
|
|
18
|
+
if (!type)
|
|
19
|
+
return schema;
|
|
20
|
+
const types = Array.isArray(type) ? type : [type];
|
|
21
|
+
if (types.includes('null'))
|
|
22
|
+
return schema;
|
|
23
|
+
return Type.Unsafe({ ...schema, type: [...types, 'null'] });
|
|
24
|
+
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { type TObject } from '@sinclair/typebox';
|
|
2
2
|
import type { DbTables } from '../../types.js';
|
|
3
3
|
export declare function SearchTableBodyPost(dbTables: DbTables, tableName: string): TObject;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Querystring schema, parameterized on the plugin's `maxItemsPerPage`.
|
|
6
|
+
*
|
|
7
|
+
* The `itemsPerPage` default MUST NOT exceed the cap: Ajv injects the default before any
|
|
8
|
+
* runtime check runs, so a fixed default of 500 combined with `maxItemsPerPage: 100` would
|
|
9
|
+
* make every request without an explicit `itemsPerPage` fail with 400. The default is
|
|
10
|
+
* therefore `min(500, cap)` and the cap doubles as the schema `maximum` (structured 400).
|
|
11
|
+
*/
|
|
12
|
+
export declare function SearchTableQuery(maxItemsPerPage?: number): TObject;
|
|
13
|
+
/** Default-cap variant, kept for granular composition and backward compatibility. */
|
|
14
|
+
export declare const SearchTableQueryString: TObject<import("@sinclair/typebox").TProperties>;
|
|
13
15
|
export declare function SearchTableResponse(dbTables: DbTables, tableName: string): TObject;
|
|
14
16
|
//# sourceMappingURL=search.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAIrE,OAAO,KAAK,EAAE,QAAQ,EAA4B,MAAM,gBAAgB,CAAC;AAyBzE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAuHlF;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,eAAe,GAAE,MAAmC,GAAG,OAAO,CAe9F;AAED,qFAAqF;AACrF,eAAO,MAAM,sBAAsB,kDAAqB,CAAC;AAEzD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAsElF"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from '@sinclair/typebox';
|
|
2
2
|
import { ALLOWED_METHODS } from '../condition-methods.js';
|
|
3
3
|
import { readableFieldNames } from '../read-access.js';
|
|
4
|
+
import { DEFAULT_MAX_ITEMS_PER_PAGE } from '../../types.js';
|
|
4
5
|
/** Schema fields minus the ones hidden by `readExclude`. */
|
|
5
6
|
function readableFields(schema, tableConf) {
|
|
6
7
|
if (!tableConf?.readExclude?.length)
|
|
@@ -131,16 +132,32 @@ export function SearchTableBodyPost(dbTables, tableName) {
|
|
|
131
132
|
}
|
|
132
133
|
return Type.Object(bodyProperties);
|
|
133
134
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Querystring schema, parameterized on the plugin's `maxItemsPerPage`.
|
|
137
|
+
*
|
|
138
|
+
* The `itemsPerPage` default MUST NOT exceed the cap: Ajv injects the default before any
|
|
139
|
+
* runtime check runs, so a fixed default of 500 combined with `maxItemsPerPage: 100` would
|
|
140
|
+
* make every request without an explicit `itemsPerPage` fail with 400. The default is
|
|
141
|
+
* therefore `min(500, cap)` and the cap doubles as the schema `maximum` (structured 400).
|
|
142
|
+
*/
|
|
143
|
+
export function SearchTableQuery(maxItemsPerPage = DEFAULT_MAX_ITEMS_PER_PAGE) {
|
|
144
|
+
return Type.Object({
|
|
145
|
+
orderBy: Type.Optional(Type.String()),
|
|
146
|
+
page: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
147
|
+
itemsPerPage: Type.Optional(Type.Integer({
|
|
148
|
+
minimum: 1,
|
|
149
|
+
maximum: maxItemsPerPage,
|
|
150
|
+
default: Math.min(500, maxItemsPerPage),
|
|
151
|
+
})),
|
|
152
|
+
computeMin: Type.Optional(Type.String()),
|
|
153
|
+
computeMax: Type.Optional(Type.String()),
|
|
154
|
+
computeSum: Type.Optional(Type.String()),
|
|
155
|
+
computeAvg: Type.Optional(Type.String()),
|
|
156
|
+
// selectComputed list goes in the body (POST), not querystring — see SearchTableBodyPost.
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/** Default-cap variant, kept for granular composition and backward compatibility. */
|
|
160
|
+
export const SearchTableQueryString = SearchTableQuery();
|
|
144
161
|
export function SearchTableResponse(dbTables, tableName) {
|
|
145
162
|
const tableConf = dbTables[tableName];
|
|
146
163
|
// Computed fields are present in main rows only when explicitly listed in
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAiC,MAAM,wBAAwB,CAAC;AACzH,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,MAAM,EACP,MAAM,aAAa,CAAC;AAarB,KAAK,UAAU,CAAC,EAAE,IAAI,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAExH,wBAAgB,eAAe,CAC7B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,EAAE,SAAS,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,EAErE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,YAAY,GAAE,EAAa,EAC3B,iBAAiB,CAAC,EAAE,CAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,yBAAyB;CAAE,KAC3E,IAAI,GACR;IAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAiC,MAAM,wBAAwB,CAAC;AACzH,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,MAAM,EACP,MAAM,aAAa,CAAC;AAarB,KAAK,UAAU,CAAC,EAAE,IAAI,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAExH,wBAAgB,eAAe,CAC7B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,EAAE,SAAS,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,EAErE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,YAAY,GAAE,EAAa,EAC3B,iBAAiB,CAAC,EAAE,CAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,yBAAyB;CAAE,KAC3E,IAAI,GACR;IAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAsChG;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAChB,MAAM,CAAC,CAAC,CAAC,CAMX;AAkED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,aAAa,CAC3B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjC,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,EAClD,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,EAC3B,OAAO,CAAC,EAAE,oBAAoB,GAC7B,cAAc,CAShB;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,GAC5B,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAE9B;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,KAAK,EAAE,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,GACvC,GAAG,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAEjC"}
|
|
@@ -17,8 +17,18 @@ export function exportTableInfo(Schema, extraFilters = {}, extendedCondition) {
|
|
|
17
17
|
// qualifier (a subquery alias) takes precedence over the table name.
|
|
18
18
|
const table = qi(qualifier ?? Schema.tableName, dialect);
|
|
19
19
|
for (const field of Object.keys(Schema.fields)) {
|
|
20
|
-
if (field in filterValues
|
|
21
|
-
|
|
20
|
+
if (!(field in filterValues) || filterValues[field] === undefined)
|
|
21
|
+
continue;
|
|
22
|
+
const col = `${table}.${qi(Schema.col(field), dialect)}`;
|
|
23
|
+
// An explicit null filters by IS NULL: equality with NULL never matches, and
|
|
24
|
+
// silently dropping the filter would return unfiltered results for a request
|
|
25
|
+
// that asked for "field is null".
|
|
26
|
+
if (filterValues[field] === null) {
|
|
27
|
+
// Second arg is CB's enable-guard: without `true` the call is a no-op.
|
|
28
|
+
condition.isNull(col, true);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
condition.isEqual(col, filterValues[field]);
|
|
22
32
|
}
|
|
23
33
|
}
|
|
24
34
|
// extraFilters are handled exclusively by extendedCondition
|