@quadrokit/client 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.mjs +69 -0
- package/dist/generate/codegen.d.ts +3 -0
- package/dist/generate/codegen.d.ts.map +1 -0
- package/dist/generate/codegen.mjs +287 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/{src/index.ts → dist/index.mjs} +1 -1
- package/dist/runtime/collection.d.ts +29 -0
- package/dist/runtime/collection.d.ts.map +1 -0
- package/dist/runtime/collection.mjs +94 -0
- package/dist/runtime/data-class.d.ts +42 -0
- package/dist/runtime/data-class.d.ts.map +1 -0
- package/dist/runtime/data-class.mjs +99 -0
- package/dist/runtime/datastore.d.ts +8 -0
- package/dist/runtime/datastore.d.ts.map +1 -0
- package/dist/runtime/datastore.mjs +15 -0
- package/dist/runtime/errors.d.ts +6 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/errors.mjs +10 -0
- package/dist/runtime/http.d.ts +16 -0
- package/dist/runtime/http.d.ts.map +1 -0
- package/dist/runtime/http.mjs +54 -0
- package/dist/runtime/index.d.ts +9 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.mjs +7 -0
- package/dist/runtime/paths.d.ts +16 -0
- package/dist/runtime/paths.d.ts.map +1 -0
- package/dist/runtime/paths.mjs +2 -0
- package/dist/runtime/query.d.ts +14 -0
- package/dist/runtime/query.d.ts.map +1 -0
- package/dist/runtime/query.mjs +76 -0
- package/dist/runtime/unwrap.d.ts +4 -0
- package/dist/runtime/unwrap.d.ts.map +1 -0
- package/dist/runtime/unwrap.mjs +31 -0
- package/package.json +14 -7
- package/src/cli.ts +0 -82
- package/src/generate/codegen.ts +0 -317
- package/src/runtime/collection.ts +0 -135
- package/src/runtime/data-class.ts +0 -156
- package/src/runtime/datastore.ts +0 -25
- package/src/runtime/errors.ts +0 -11
- package/src/runtime/http.ts +0 -64
- package/src/runtime/index.ts +0 -23
- package/src/runtime/paths.ts +0 -42
- package/src/runtime/query.ts +0 -104
- package/src/runtime/unwrap.ts +0 -33
- package/tsconfig.json +0 -9
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { writeGenerated } from './generate/codegen.mjs';
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
let command = '';
|
|
7
|
+
let url;
|
|
8
|
+
let token;
|
|
9
|
+
let out = '.quadrokit/generated';
|
|
10
|
+
for (let i = 0; i < argv.length; i++) {
|
|
11
|
+
const a = argv[i];
|
|
12
|
+
if (a === '--url' && argv[i + 1]) {
|
|
13
|
+
url = argv[++i];
|
|
14
|
+
}
|
|
15
|
+
else if (a === '--token' && argv[i + 1]) {
|
|
16
|
+
token = argv[++i];
|
|
17
|
+
}
|
|
18
|
+
else if (a === '--out' && argv[i + 1]) {
|
|
19
|
+
out = argv[++i];
|
|
20
|
+
}
|
|
21
|
+
else if (!a.startsWith('-') && !command) {
|
|
22
|
+
command = a;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { command, url, token, out };
|
|
26
|
+
}
|
|
27
|
+
async function loadCatalog(url, token) {
|
|
28
|
+
if (url.startsWith('file:')) {
|
|
29
|
+
const path = fileURLToPath(url);
|
|
30
|
+
const text = await readFile(path, 'utf8');
|
|
31
|
+
return JSON.parse(text);
|
|
32
|
+
}
|
|
33
|
+
const headers = {
|
|
34
|
+
Accept: 'application/json',
|
|
35
|
+
};
|
|
36
|
+
if (token) {
|
|
37
|
+
headers.Authorization = `Bearer ${token}`;
|
|
38
|
+
}
|
|
39
|
+
const res = await fetch(url, { headers });
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
const t = await res.text();
|
|
42
|
+
throw new Error(`Failed to fetch catalog (${res.status}): ${t.slice(0, 500)}`);
|
|
43
|
+
}
|
|
44
|
+
return res.json();
|
|
45
|
+
}
|
|
46
|
+
async function main() {
|
|
47
|
+
const argv = process.argv.slice(2);
|
|
48
|
+
const { command, url, token, out } = parseArgs(argv);
|
|
49
|
+
if (command !== 'generate') {
|
|
50
|
+
console.error(`Usage: quadrokit-client generate --url <catalog_url> [--token <token>] [--out <dir>]
|
|
51
|
+
|
|
52
|
+
Examples:
|
|
53
|
+
quadrokit-client generate --url http://localhost:7080/rest/\\$catalog --token secret
|
|
54
|
+
quadrokit-client generate --url file://./assets/catalog.json --out .quadrokit/generated
|
|
55
|
+
`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
if (!url) {
|
|
59
|
+
console.error('Error: --url is required for generate.');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const catalog = await loadCatalog(url, token);
|
|
63
|
+
await writeGenerated(out, catalog);
|
|
64
|
+
console.error(`Wrote generated client to ${out}`);
|
|
65
|
+
}
|
|
66
|
+
main().catch((e) => {
|
|
67
|
+
console.error(e);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codegen.d.ts","sourceRoot":"","sources":["../../src/generate/codegen.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAsC,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAgTxF,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBxF"}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { sessionCookieName } from '@quadrokit/shared';
|
|
4
|
+
function map4dType(attr) {
|
|
5
|
+
const t = attr.type ?? 'unknown';
|
|
6
|
+
switch (t) {
|
|
7
|
+
case 'long':
|
|
8
|
+
case 'word':
|
|
9
|
+
return 'number';
|
|
10
|
+
case 'string':
|
|
11
|
+
return 'string';
|
|
12
|
+
case 'bool':
|
|
13
|
+
return 'boolean';
|
|
14
|
+
case 'number':
|
|
15
|
+
return 'number';
|
|
16
|
+
case 'date':
|
|
17
|
+
return 'string';
|
|
18
|
+
case 'duration':
|
|
19
|
+
return 'string | number';
|
|
20
|
+
case 'image':
|
|
21
|
+
return 'string | null';
|
|
22
|
+
case 'object':
|
|
23
|
+
return 'Record<string, unknown>';
|
|
24
|
+
default: {
|
|
25
|
+
if (attr.kind === 'relatedEntity') {
|
|
26
|
+
return `${t} | null`;
|
|
27
|
+
}
|
|
28
|
+
if (attr.kind === 'relatedEntities' || attr.kind === 'calculated') {
|
|
29
|
+
return 'unknown';
|
|
30
|
+
}
|
|
31
|
+
return 'unknown';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function selectionToEntity(selectionType) {
|
|
36
|
+
if (selectionType.endsWith('Selection')) {
|
|
37
|
+
return selectionType.slice(0, -'Selection'.length);
|
|
38
|
+
}
|
|
39
|
+
return selectionType;
|
|
40
|
+
}
|
|
41
|
+
function navigableRelations(dc) {
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const a of dc.attributes ?? []) {
|
|
44
|
+
if (a.kind === 'relatedEntities') {
|
|
45
|
+
out.push({
|
|
46
|
+
attr: a.name,
|
|
47
|
+
targetClass: selectionToEntity(a.type ?? 'unknown'),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (a.kind === 'calculated' && a.behavior === 'relatedEntities' && a.type) {
|
|
51
|
+
out.push({ attr: a.name, targetClass: selectionToEntity(a.type) });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
function relationTargets(dc) {
|
|
57
|
+
const m = {};
|
|
58
|
+
for (const a of dc.attributes ?? []) {
|
|
59
|
+
if (a.kind === 'relatedEntity' && a.type) {
|
|
60
|
+
m[a.name] = a.type;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return m;
|
|
64
|
+
}
|
|
65
|
+
function keyNames(dc) {
|
|
66
|
+
const k = dc.key?.map((x) => x.name) ?? ['ID'];
|
|
67
|
+
return k.length ? k : ['ID'];
|
|
68
|
+
}
|
|
69
|
+
function collectPaths(dc, byName, maxDepth) {
|
|
70
|
+
const paths = new Set();
|
|
71
|
+
const visit = (current, prefix, depth) => {
|
|
72
|
+
for (const a of current.attributes ?? []) {
|
|
73
|
+
if (a.kind === 'storage' || a.kind === 'calculated' || a.kind === 'alias') {
|
|
74
|
+
const p = prefix ? `${prefix}.${a.name}` : a.name;
|
|
75
|
+
paths.add(p);
|
|
76
|
+
}
|
|
77
|
+
if (a.kind === 'relatedEntity' && a.type && depth < maxDepth) {
|
|
78
|
+
const child = byName.get(a.type);
|
|
79
|
+
const base = prefix ? `${prefix}.${a.name}` : a.name;
|
|
80
|
+
paths.add(base);
|
|
81
|
+
if (child) {
|
|
82
|
+
visit(child, base, depth + 1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
visit(dc, '', 0);
|
|
88
|
+
return [...paths].sort();
|
|
89
|
+
}
|
|
90
|
+
function emitInterface(dc, _byName) {
|
|
91
|
+
const lines = [];
|
|
92
|
+
lines.push(`export interface ${dc.name} {`);
|
|
93
|
+
for (const a of dc.attributes ?? []) {
|
|
94
|
+
if (a.kind === 'relatedEntities') {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (a.kind === 'calculated' && a.behavior === 'relatedEntities') {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const opt = a.not_null || a.identifying ? '' : '?';
|
|
101
|
+
const ts = map4dType(a);
|
|
102
|
+
lines.push(` ${a.name}${opt}: ${ts};`);
|
|
103
|
+
}
|
|
104
|
+
lines.push('}');
|
|
105
|
+
return lines.join('\n');
|
|
106
|
+
}
|
|
107
|
+
function emitTypes(catalog) {
|
|
108
|
+
const classes = catalog.dataClasses?.filter((d) => d.exposed) ?? [];
|
|
109
|
+
const byName = new Map(classes.map((c) => [c.name, c]));
|
|
110
|
+
const interfaces = classes.map((c) => emitInterface(c, byName));
|
|
111
|
+
const pathTypes = classes.map((c) => {
|
|
112
|
+
const paths = collectPaths(c, byName, 2);
|
|
113
|
+
const name = `${c.name}Path`;
|
|
114
|
+
const body = paths.length ? paths.map((p) => `'${p}'`).join('\n | ') : 'never';
|
|
115
|
+
return `export type ${name} =\n | ${body};`;
|
|
116
|
+
});
|
|
117
|
+
return `/* eslint-disable */\n/* Auto-generated by @quadrokit/client — do not edit */\n\n${interfaces.join('\n\n')}\n\n${pathTypes.join('\n\n')}\n`;
|
|
118
|
+
}
|
|
119
|
+
function emitClient(catalog) {
|
|
120
|
+
const classes = catalog.dataClasses?.filter((d) => d.exposed) ?? [];
|
|
121
|
+
const dbName = catalog.__NAME ?? 'default';
|
|
122
|
+
const hasAuthentify = catalog.methods?.some((m) => m.name === 'authentify' && m.applyTo === 'dataStore');
|
|
123
|
+
const keyNamesRecord = Object.fromEntries(classes.map((x) => [x.name, keyNames(x)]));
|
|
124
|
+
const imports = classes.length > 0
|
|
125
|
+
? `/* eslint-disable */\n/* Auto-generated by @quadrokit/client — do not edit */\n\nimport {\n QuadroHttp,\n makeDataClassApi,\n attachRelatedApis,\n callDatastorePath,\n type CollectionHandle,\n type CollectionOptions,\n type SelectedEntity,\n} from '@quadrokit/client/runtime';\n`
|
|
126
|
+
: `/* eslint-disable */\n/* Auto-generated by @quadrokit/client — do not edit */\n\nimport { QuadroHttp, callDatastorePath } from '@quadrokit/client/runtime';\n`;
|
|
127
|
+
const typeImports = classes.map((c) => c.name).join(', ');
|
|
128
|
+
const pathImports = classes.map((c) => `${c.name}Path`).join(', ');
|
|
129
|
+
const typeImportLine = typeImports || pathImports
|
|
130
|
+
? `\nimport type { ${[typeImports, pathImports].filter(Boolean).join(', ')} } from './types.gen.mjs';\n\n`
|
|
131
|
+
: '\n';
|
|
132
|
+
const header = `${imports}${typeImportLine}`;
|
|
133
|
+
const metaExport = `
|
|
134
|
+
export const quadrokitCatalogMeta = {
|
|
135
|
+
__NAME: ${JSON.stringify(dbName)},
|
|
136
|
+
sessionCookieName: ${JSON.stringify(sessionCookieName(catalog))},
|
|
137
|
+
} as const;
|
|
138
|
+
`;
|
|
139
|
+
const configInterface = `
|
|
140
|
+
export interface QuadroClientConfig {
|
|
141
|
+
baseURL: string;
|
|
142
|
+
fetchImpl?: typeof fetch;
|
|
143
|
+
defaultHeaders?: Record<string, string>;
|
|
144
|
+
}
|
|
145
|
+
`;
|
|
146
|
+
const mapFunctions = classes
|
|
147
|
+
.map((c) => {
|
|
148
|
+
const nav = navigableRelations(c);
|
|
149
|
+
const relMap = JSON.stringify(relationTargets(c));
|
|
150
|
+
return ` function map${c.name}Row(http: QuadroHttp, raw: unknown): ${c.name} {
|
|
151
|
+
const row = raw as ${c.name};
|
|
152
|
+
const pk = (row as { ID?: number }).ID ?? (row as { id?: number }).id;
|
|
153
|
+
attachRelatedApis(
|
|
154
|
+
raw,
|
|
155
|
+
{
|
|
156
|
+
http,
|
|
157
|
+
parentClass: '${c.name}',
|
|
158
|
+
parentId: pk as number,
|
|
159
|
+
relationMap: ${relMap},
|
|
160
|
+
keyNames: ${JSON.stringify(keyNamesRecord)},
|
|
161
|
+
},
|
|
162
|
+
${JSON.stringify(nav)},
|
|
163
|
+
);
|
|
164
|
+
return row;
|
|
165
|
+
}`;
|
|
166
|
+
})
|
|
167
|
+
.join('\n\n');
|
|
168
|
+
const classBranches = classes
|
|
169
|
+
.map((c) => {
|
|
170
|
+
const relMap = JSON.stringify(relationTargets(c));
|
|
171
|
+
const kn = JSON.stringify(keyNames(c));
|
|
172
|
+
const pathsType = `${c.name}Path`;
|
|
173
|
+
return ` ${c.name}: (() => {
|
|
174
|
+
const cfg = {
|
|
175
|
+
http,
|
|
176
|
+
className: '${c.name}',
|
|
177
|
+
relationMap: ${relMap},
|
|
178
|
+
keyNames: ${kn},
|
|
179
|
+
} as const;
|
|
180
|
+
const api = makeDataClassApi<${c.name}>(cfg);
|
|
181
|
+
return {
|
|
182
|
+
all<S extends readonly ${pathsType}[] = readonly []>(
|
|
183
|
+
options?: CollectionOptions & { select?: S },
|
|
184
|
+
): CollectionHandle<S['length'] extends 0 ? ${c.name} : SelectedEntity<${c.name}, S>> {
|
|
185
|
+
const inner = api.all(options as CollectionOptions);
|
|
186
|
+
return {
|
|
187
|
+
...inner,
|
|
188
|
+
delete: inner.delete.bind(inner),
|
|
189
|
+
release: inner.release.bind(inner),
|
|
190
|
+
get length() {
|
|
191
|
+
return inner.length;
|
|
192
|
+
},
|
|
193
|
+
[Symbol.asyncIterator]() {
|
|
194
|
+
const it = inner[Symbol.asyncIterator]();
|
|
195
|
+
return {
|
|
196
|
+
async next() {
|
|
197
|
+
const n = await it.next();
|
|
198
|
+
if (!n.done && n.value) {
|
|
199
|
+
map${c.name}Row(http, n.value);
|
|
200
|
+
}
|
|
201
|
+
return n;
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
} as CollectionHandle<S['length'] extends 0 ? ${c.name} : SelectedEntity<${c.name}, S>>;
|
|
206
|
+
},
|
|
207
|
+
async get<S extends readonly ${pathsType}[] = readonly []>(
|
|
208
|
+
id: string | number,
|
|
209
|
+
options?: { select?: S },
|
|
210
|
+
): Promise<(S['length'] extends 0 ? ${c.name} : SelectedEntity<${c.name}, S>) | null> {
|
|
211
|
+
const entity = await api.get(id, options);
|
|
212
|
+
if (entity) {
|
|
213
|
+
map${c.name}Row(http, entity);
|
|
214
|
+
}
|
|
215
|
+
return entity as (S['length'] extends 0 ? ${c.name} : SelectedEntity<${c.name}, S>) | null;
|
|
216
|
+
},
|
|
217
|
+
delete: (id: string | number) => api.delete(id),
|
|
218
|
+
query<S extends readonly ${pathsType}[] = readonly []>(
|
|
219
|
+
filter: string,
|
|
220
|
+
options?: CollectionOptions & { params?: unknown[]; select?: S },
|
|
221
|
+
): CollectionHandle<S['length'] extends 0 ? ${c.name} : SelectedEntity<${c.name}, S>> {
|
|
222
|
+
const inner = api.query(filter, options as CollectionOptions);
|
|
223
|
+
return {
|
|
224
|
+
...inner,
|
|
225
|
+
delete: inner.delete.bind(inner),
|
|
226
|
+
release: inner.release.bind(inner),
|
|
227
|
+
get length() {
|
|
228
|
+
return inner.length;
|
|
229
|
+
},
|
|
230
|
+
[Symbol.asyncIterator]() {
|
|
231
|
+
const it = inner[Symbol.asyncIterator]();
|
|
232
|
+
return {
|
|
233
|
+
async next() {
|
|
234
|
+
const n = await it.next();
|
|
235
|
+
if (!n.done && n.value) {
|
|
236
|
+
map${c.name}Row(http, n.value);
|
|
237
|
+
}
|
|
238
|
+
return n;
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
} as CollectionHandle<S['length'] extends 0 ? ${c.name} : SelectedEntity<${c.name}, S>>;
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
})(),`;
|
|
246
|
+
})
|
|
247
|
+
.join('\n');
|
|
248
|
+
const authentifyBlock = hasAuthentify
|
|
249
|
+
? `
|
|
250
|
+
authentify: {
|
|
251
|
+
login: (body: { email: string; password: string }) =>
|
|
252
|
+
callDatastorePath(http, ['authentify', 'login'], { body }),
|
|
253
|
+
},`
|
|
254
|
+
: '';
|
|
255
|
+
const body = `
|
|
256
|
+
export function createClient(config: QuadroClientConfig) {
|
|
257
|
+
const http = new QuadroHttp({
|
|
258
|
+
baseURL: config.baseURL,
|
|
259
|
+
fetchImpl: config.fetchImpl,
|
|
260
|
+
defaultHeaders: config.defaultHeaders,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
${mapFunctions}
|
|
264
|
+
|
|
265
|
+
return {${authentifyBlock}
|
|
266
|
+
${classBranches}
|
|
267
|
+
rpc: (segments: string[], init?: { method?: 'GET' | 'POST'; body?: unknown }) =>
|
|
268
|
+
callDatastorePath(http, segments, init),
|
|
269
|
+
sessionCookieName: quadrokitCatalogMeta.sessionCookieName,
|
|
270
|
+
_http: http,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export type QuadroClient = ReturnType<typeof createClient>;
|
|
275
|
+
`;
|
|
276
|
+
return header + metaExport + configInterface + body;
|
|
277
|
+
}
|
|
278
|
+
export async function writeGenerated(outDir, catalog) {
|
|
279
|
+
await mkdir(outDir, { recursive: true });
|
|
280
|
+
await writeFile(path.join(outDir, 'types.gen.ts'), emitTypes(catalog), 'utf8');
|
|
281
|
+
await writeFile(path.join(outDir, 'client.gen.ts'), emitClient(catalog), 'utf8');
|
|
282
|
+
await writeFile(path.join(outDir, 'meta.json'), JSON.stringify({
|
|
283
|
+
__NAME: catalog.__NAME,
|
|
284
|
+
sessionCookieName: sessionCookieName(catalog),
|
|
285
|
+
generatedAt: new Date().toISOString(),
|
|
286
|
+
}, null, 2), 'utf8');
|
|
287
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,oBAAoB,CAAA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { QuadroHttp } from './http.js';
|
|
2
|
+
import { type ListQueryParams } from './query.js';
|
|
3
|
+
export interface CollectionContext {
|
|
4
|
+
http: QuadroHttp;
|
|
5
|
+
className: string;
|
|
6
|
+
relationMap: Record<string, string>;
|
|
7
|
+
/** Primary key attribute names (first wins for simple path). */
|
|
8
|
+
keyNames: readonly string[];
|
|
9
|
+
path: string;
|
|
10
|
+
/** Optional entity-set URL from a previous response for `release()`. */
|
|
11
|
+
entitySetUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CollectionOptions extends ListQueryParams {
|
|
14
|
+
page?: number;
|
|
15
|
+
pageSize?: number;
|
|
16
|
+
select?: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
export type MapRow<R> = (raw: unknown) => R;
|
|
19
|
+
/**
|
|
20
|
+
* Lazy paged collection: async iteration loads further pages; tracks PKs for `delete()`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createCollection<R>(ctx: CollectionContext, initialOptions: CollectionOptions, mapRow: MapRow<R>): CollectionHandle<R>;
|
|
23
|
+
export type CollectionHandle<R> = AsyncIterable<R> & {
|
|
24
|
+
delete(): Promise<void>;
|
|
25
|
+
release(): Promise<void>;
|
|
26
|
+
/** Number of entities seen while iterating (used after partial iteration). */
|
|
27
|
+
readonly length: number;
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=collection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../../src/runtime/collection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,YAAY,CAAA;AAGxE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,UAAU,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,gEAAgE;IAChE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC3B;AAED,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,KAAK,CAAC,CAAA;AAe3C;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,GAAG,EAAE,iBAAiB,EACtB,cAAc,EAAE,iBAAiB,EACjC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAChB,gBAAgB,CAAC,CAAC,CAAC,CAoFrB;AAED,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,GAAG;IACnD,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACvB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACxB,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB,CAAA"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { buildListSearchParams } from './query.mjs';
|
|
2
|
+
import { unwrapEntityList } from './unwrap.mjs';
|
|
3
|
+
function primaryKeyFromRow(row, keyNames) {
|
|
4
|
+
if (!row || typeof row !== 'object') {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
const o = row;
|
|
8
|
+
for (const k of keyNames) {
|
|
9
|
+
if (k in o && o[k] != null) {
|
|
10
|
+
return o[k];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Lazy paged collection: async iteration loads further pages; tracks PKs for `delete()`.
|
|
17
|
+
*/
|
|
18
|
+
export function createCollection(ctx, initialOptions, mapRow) {
|
|
19
|
+
const seenIds = new Set();
|
|
20
|
+
let entitySetUrl = ctx.entitySetUrl;
|
|
21
|
+
async function fetchPage(page) {
|
|
22
|
+
const qs = buildListSearchParams(ctx.className, { ...initialOptions, page }, ctx.relationMap);
|
|
23
|
+
const res = await ctx.http.request(`${ctx.path}${qs}`);
|
|
24
|
+
const text = await res.text();
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
throw new Error(`List failed ${res.status}: ${text.slice(0, 200)}`);
|
|
27
|
+
}
|
|
28
|
+
const loc = res.headers.get('Content-Location') ?? res.headers.get('Location');
|
|
29
|
+
if (loc) {
|
|
30
|
+
entitySetUrl = loc.startsWith('http')
|
|
31
|
+
? loc
|
|
32
|
+
: `${ctx.http.baseURL}${loc.startsWith('/') ? '' : '/'}${loc}`;
|
|
33
|
+
}
|
|
34
|
+
const json = text ? JSON.parse(text) : [];
|
|
35
|
+
return unwrapEntityList(ctx.className, json);
|
|
36
|
+
}
|
|
37
|
+
async function* iteratePages() {
|
|
38
|
+
let page = initialOptions.page ?? 1;
|
|
39
|
+
const pageSize = initialOptions.pageSize ?? 50;
|
|
40
|
+
while (true) {
|
|
41
|
+
const rows = await fetchPage(page);
|
|
42
|
+
if (!rows.length) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
for (const raw of rows) {
|
|
46
|
+
const mapped = mapRow(raw);
|
|
47
|
+
const pk = primaryKeyFromRow(raw, ctx.keyNames);
|
|
48
|
+
if (pk !== undefined) {
|
|
49
|
+
seenIds.add(pk);
|
|
50
|
+
}
|
|
51
|
+
yield mapped;
|
|
52
|
+
}
|
|
53
|
+
if (rows.length < pageSize) {
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
page += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const iterable = {
|
|
60
|
+
[Symbol.asyncIterator]() {
|
|
61
|
+
return iteratePages()[Symbol.asyncIterator]();
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
const handle = {
|
|
65
|
+
...iterable,
|
|
66
|
+
async delete() {
|
|
67
|
+
const ids = [...seenIds];
|
|
68
|
+
if (!ids.length) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
await Promise.all(ids.map((id) => ctx.http.void(`${ctx.className}(${encodeURIComponent(String(id))})`, {
|
|
72
|
+
method: 'DELETE',
|
|
73
|
+
})));
|
|
74
|
+
seenIds.clear();
|
|
75
|
+
},
|
|
76
|
+
async release() {
|
|
77
|
+
if (entitySetUrl) {
|
|
78
|
+
try {
|
|
79
|
+
await ctx.http.void(entitySetUrl.replace(ctx.http.baseURL, '') || entitySetUrl, {
|
|
80
|
+
method: 'DELETE',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Some servers ignore release; swallow
|
|
85
|
+
}
|
|
86
|
+
entitySetUrl = undefined;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
get length() {
|
|
90
|
+
return seenIds.size;
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
return handle;
|
|
94
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type CollectionHandle, type CollectionOptions } from './collection.js';
|
|
2
|
+
import type { QuadroHttp } from './http.js';
|
|
3
|
+
import { unwrapEntityList } from './unwrap.js';
|
|
4
|
+
export interface DataClassRuntimeConfig {
|
|
5
|
+
http: QuadroHttp;
|
|
6
|
+
className: string;
|
|
7
|
+
/** relation attribute name → target class name for $expand */
|
|
8
|
+
relationMap: Record<string, string>;
|
|
9
|
+
keyNames: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function makeDataClassApi<R = unknown>(cfg: DataClassRuntimeConfig): {
|
|
12
|
+
all<S extends readonly string[] = readonly []>(options?: CollectionOptions & {
|
|
13
|
+
select?: S;
|
|
14
|
+
}): CollectionHandle<S extends readonly never[] ? R : R>;
|
|
15
|
+
get<S extends readonly string[] = readonly []>(id: string | number, options?: {
|
|
16
|
+
select?: S;
|
|
17
|
+
}): Promise<R | null>;
|
|
18
|
+
delete(id: string | number): Promise<boolean>;
|
|
19
|
+
query<S extends readonly string[] = readonly []>(filter: string, options?: CollectionOptions & {
|
|
20
|
+
params?: unknown[];
|
|
21
|
+
select?: S;
|
|
22
|
+
}): CollectionHandle<S extends readonly never[] ? R : R>;
|
|
23
|
+
};
|
|
24
|
+
export interface EntityNavigationConfig {
|
|
25
|
+
http: QuadroHttp;
|
|
26
|
+
parentClass: string;
|
|
27
|
+
parentId: string | number;
|
|
28
|
+
relationMap: Record<string, string>;
|
|
29
|
+
keyNames: Record<string, readonly string[]>;
|
|
30
|
+
}
|
|
31
|
+
/** Nested collection under an entity, e.g. `Agency(1)/todayBookings`. */
|
|
32
|
+
export declare function makeRelatedCollectionApi(cfg: EntityNavigationConfig, attributeName: string, targetClassName: string): {
|
|
33
|
+
list<S extends readonly string[] = readonly []>(options?: CollectionOptions & {
|
|
34
|
+
select?: S;
|
|
35
|
+
}): CollectionHandle<unknown>;
|
|
36
|
+
};
|
|
37
|
+
export declare function attachRelatedApis(row: unknown, cfg: EntityNavigationConfig, relations: readonly {
|
|
38
|
+
attr: string;
|
|
39
|
+
targetClass: string;
|
|
40
|
+
}[]): void;
|
|
41
|
+
export { unwrapEntityList };
|
|
42
|
+
//# sourceMappingURL=data-class.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-class.d.ts","sourceRoot":"","sources":["../../src/runtime/data-class.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,iBAAiB,EAAoB,MAAM,iBAAiB,CAAA;AACjG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C,OAAO,EAAgB,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAE5D,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,UAAU,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAC5B;AAED,wBAAgB,gBAAgB,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,sBAAsB;QAIjE,CAAC,SAAS,SAAS,MAAM,EAAE,0BACnB,iBAAiB,GAAG;QAAE,MAAM,CAAC,EAAE,CAAC,CAAA;KAAE,GAC3C,gBAAgB,CAAC,CAAC,SAAS,SAAS,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;QAc7C,CAAC,SAAS,SAAS,MAAM,EAAE,oBAC/B,MAAM,GAAG,MAAM,YACT;QAAE,MAAM,CAAC,EAAE,CAAC,CAAA;KAAE,GACvB,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;eASH,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;UAU7C,CAAC,SAAS,SAAS,MAAM,EAAE,wBACvB,MAAM,YACJ,iBAAiB,GAAG;QAAE,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;QAAC,MAAM,CAAC,EAAE,CAAC,CAAA;KAAE,GAC/D,gBAAgB,CAAC,CAAC,SAAS,SAAS,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;EAe1D;AA6BD,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,UAAU,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAA;CAC5C;AAED,yEAAyE;AACzE,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,sBAAsB,EAC3B,aAAa,EAAE,MAAM,EACrB,eAAe,EAAE,MAAM;SAKhB,CAAC,SAAS,SAAS,MAAM,EAAE,0BACpB,iBAAiB,GAAG;QAAE,MAAM,CAAC,EAAE,CAAC,CAAA;KAAE,GAC3C,gBAAgB,CAAC,OAAO,CAAC;EAc/B;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EAAE,GAC1D,IAAI,CAYN;AAED,OAAO,EAAE,gBAAgB,EAAE,CAAA"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createCollection } from './collection.mjs';
|
|
2
|
+
import { buildEntityParams } from './query.mjs';
|
|
3
|
+
import { unwrapEntity, unwrapEntityList } from './unwrap.mjs';
|
|
4
|
+
export function makeDataClassApi(cfg) {
|
|
5
|
+
const path = `/${cfg.className}`;
|
|
6
|
+
return {
|
|
7
|
+
all(options) {
|
|
8
|
+
return createCollection({
|
|
9
|
+
http: cfg.http,
|
|
10
|
+
className: cfg.className,
|
|
11
|
+
relationMap: cfg.relationMap,
|
|
12
|
+
keyNames: cfg.keyNames,
|
|
13
|
+
path,
|
|
14
|
+
}, options ?? {}, (raw) => raw);
|
|
15
|
+
},
|
|
16
|
+
async get(id, options) {
|
|
17
|
+
const qs = options?.select?.length
|
|
18
|
+
? buildEntityParams(cfg.className, options.select, cfg.relationMap)
|
|
19
|
+
: '';
|
|
20
|
+
const key = encodeURIComponent(String(id));
|
|
21
|
+
const body = await cfg.http.json(`${cfg.className}(${key})${qs}`);
|
|
22
|
+
return unwrapEntity(cfg.className, body);
|
|
23
|
+
},
|
|
24
|
+
async delete(id) {
|
|
25
|
+
const key = encodeURIComponent(String(id));
|
|
26
|
+
try {
|
|
27
|
+
await cfg.http.void(`${cfg.className}(${key})`, { method: 'DELETE' });
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
query(filter, options) {
|
|
35
|
+
const paramFilter = substituteQueryParams(filter, options?.params);
|
|
36
|
+
return createCollection({
|
|
37
|
+
http: cfg.http,
|
|
38
|
+
className: cfg.className,
|
|
39
|
+
relationMap: cfg.relationMap,
|
|
40
|
+
keyNames: cfg.keyNames,
|
|
41
|
+
path,
|
|
42
|
+
}, { ...options, filter: paramFilter }, (raw) => raw);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function substituteQueryParams(expr, params) {
|
|
47
|
+
if (!params?.length) {
|
|
48
|
+
return expr;
|
|
49
|
+
}
|
|
50
|
+
let out = expr;
|
|
51
|
+
params.forEach((p, i) => {
|
|
52
|
+
const placeholder = `:${i + 1}`;
|
|
53
|
+
const lit = formatFilterLiteral(p);
|
|
54
|
+
out = out.split(placeholder).join(lit);
|
|
55
|
+
});
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
function formatFilterLiteral(p) {
|
|
59
|
+
if (p === null || p === undefined) {
|
|
60
|
+
return 'null';
|
|
61
|
+
}
|
|
62
|
+
if (typeof p === 'number') {
|
|
63
|
+
return String(p);
|
|
64
|
+
}
|
|
65
|
+
if (typeof p === 'boolean') {
|
|
66
|
+
return p ? 'true' : 'false';
|
|
67
|
+
}
|
|
68
|
+
const s = String(p).replace(/'/g, "''");
|
|
69
|
+
return `'${s}'`;
|
|
70
|
+
}
|
|
71
|
+
/** Nested collection under an entity, e.g. `Agency(1)/todayBookings`. */
|
|
72
|
+
export function makeRelatedCollectionApi(cfg, attributeName, targetClassName) {
|
|
73
|
+
const basePath = `/${cfg.parentClass}(${encodeURIComponent(String(cfg.parentId))})/${attributeName}`;
|
|
74
|
+
return {
|
|
75
|
+
list(options) {
|
|
76
|
+
return createCollection({
|
|
77
|
+
http: cfg.http,
|
|
78
|
+
className: targetClassName,
|
|
79
|
+
relationMap: cfg.relationMap,
|
|
80
|
+
keyNames: cfg.keyNames[targetClassName] ?? ['ID'],
|
|
81
|
+
path: basePath,
|
|
82
|
+
}, options ?? {}, (raw) => raw);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function attachRelatedApis(row, cfg, relations) {
|
|
87
|
+
if (!row || typeof row !== 'object') {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const obj = row;
|
|
91
|
+
for (const { attr, targetClass } of relations) {
|
|
92
|
+
Object.defineProperty(obj, attr, {
|
|
93
|
+
enumerable: false,
|
|
94
|
+
configurable: true,
|
|
95
|
+
value: makeRelatedCollectionApi(cfg, attr, targetClass),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export { unwrapEntityList };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { QuadroHttp } from './http.js';
|
|
2
|
+
/** Call nested datastore paths like `authentify/login` (POST by default). */
|
|
3
|
+
export declare function callDatastorePath(http: QuadroHttp, segments: readonly string[], init?: {
|
|
4
|
+
method?: 'GET' | 'POST';
|
|
5
|
+
body?: unknown;
|
|
6
|
+
}): Promise<unknown>;
|
|
7
|
+
export declare function createDatastoreNamespace(_http: QuadroHttp, tree: Record<string, unknown>): Record<string, unknown>;
|
|
8
|
+
//# sourceMappingURL=datastore.d.ts.map
|