@quadrokit/client 0.3.14 → 0.3.16

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/README.md CHANGED
@@ -45,6 +45,7 @@ Use a URL that returns the **full** catalog (dataclasses **with attributes**). P
45
45
  | **`--login-url`** | Full login URL (default: `{catalog origin}/api/login`). |
46
46
  | **`-v` / `--verbose`** | Step-by-step logs on stderr. |
47
47
  | **`--insecure-tls`** | Skip TLS verification (dev / self-signed HTTPS only). |
48
+ | **`--no-split-type-files`** | Emit a single **`types.gen.ts`** (legacy). **Default:** split typings — **`entities/<Class>.gen.ts`**, optional **`datastore.gen.ts`**, and **`types.gen.ts`** as the barrel. |
48
49
 
49
50
  Environment variables (often via `.env` in the project root): `VITE_4D_ORIGIN`, `QUADROKIT_ACCESS_KEY`, `QUADROKIT_LOGIN_URL`, `QUADROKIT_CATALOG_TOKEN`, `QUADROKIT_GENERATE_VERBOSE`, `QUADROKIT_INSECURE_TLS`.
50
51
 
@@ -52,9 +53,13 @@ Environment variables (often via `.env` in the project root): `VITE_4D_ORIGIN`,
52
53
 
53
54
  | File | Purpose |
54
55
  |------|---------|
55
- | **`types.gen.ts`** | Entity interfaces; **`*Path`** aliases use **`QuadroAttributePaths<T>`** (recursive dot paths, depth-limited for circular relations); **`QuadroClient`** typing. |
56
+ | **`types.gen.ts`** | Barrel: **`QuadroClient`** typing; re-exports entity types and **`*Path`** aliases. |
57
+ | **`entities/<ClassName>.gen.ts`** | One file per exposed dataclass (default layout): **`export interface`** + **`export type`** **`<ClassName>Path`** (`QuadroAttributePaths<…>`). Omit with **`--no-split-type-files`** (single `types.gen.ts`). |
58
+ | **`datastore.gen.ts`** | When the catalog lists datastore REST methods (e.g. `testFn`): **`QuadroDatastoreMethodFn`**. Omitted when there are no such exposed methods. **`authentify`** is not special-cased here — it is typed under **`QuadroClient`** only when the catalog exposes it (see **`hasAuthentify`** in **`catalog.gen.json`**). |
56
59
  | **`catalog.gen.json`** | Catalog runtime spec (dataclass layouts, methods, relations) consumed by `@quadrokit/client/runtime` — keeps **`client.gen.ts`** tiny. |
57
60
  | **`client.gen.ts`** | Thin `createClient(config)` that wires `QuadroHttp` + `buildQuadroClientFromCatalogSpec` + `catalog.gen.json`. Config supports optional **`events`** (see [Request lifecycle events](#request-lifecycle-events)). |
61
+ | **`_quadroFns.gen.ts`** | Shared helpers: **`ResolveOverride`**, **`QuadroDataClassFn`**, **`QuadroEntityFn`**, **`QuadroEntityCollectionFn`**, **`QuadroDatastoreFn`** (used by **`*.gen.ts`** and optional override files). |
62
+ | **`entities/<Class>.overrides.ts`**, **`datastore.overrides.ts`** | Optional stubs you edit to **narrow method signatures** (see [Narrowing function signatures](#narrowing-function-signatures-override-files)). Created if missing; preserved across regenerate when possible. |
58
63
  | **`meta.json`** | `__NAME`, `sessionCookieName` hint for 4D session cookies. |
59
64
 
60
65
  Point your app imports at the generated folder, for example:
@@ -224,11 +229,11 @@ If your 4D project exposes methods in the REST catalog, the generator wires them
224
229
 
225
230
  | Catalog `applyTo` | Where it appears in JS | REST shape (simplified) |
226
231
  |-------------------|-------------------------|-------------------------|
227
- | **`dataClass`** | `quadro.Agency.agencyStats<R>(args, init?)` | `POST /rest/{Class}/{function}` with JSON array body |
232
+ | **`dataClass`** | `quadro.Agency.agencyStats<R, A>(args, init?)` — **`R`** = result, **`A`** = args tuple (defaults: `unknown`, `readonly unknown[]`) | `POST /rest/{Class}/{function}` with JSON array body |
228
233
  | **`entityCollection`** | On **`all()` / `query()`** handles and on related `{ list, … }` APIs | `POST /rest/{Class}/{function}` + optional `selection`, `entitySet` |
229
234
  | **`entity`** | On each **mapped entity** row | `POST /rest/{Class}({key})/{function}` |
230
235
 
231
- - **`args`**: `readonly unknown[]` — 4D receives a **JSON array** of parameters (scalars, entities, entity selections per 4D rules).
236
+ - **`args`**: tuple type **`A`** (second generic on dataclass functions; default `readonly unknown[]`) — 4D receives a **JSON array** of parameters (scalars, entities, entity selections per 4D rules).
232
237
  - **`init`**: optional `{ method: 'GET' \| 'POST', unwrapResult?, signal?, … }` — GET uses `?$params=…` for [HTTP GET functions](https://developer.4d.com/docs/ORDA/ordaClasses#onhttpget-keyword).
233
238
  - **`unwrapResult`**: when `true` (default), `{ "result": x }` responses are unwrapped to `x`.
234
239
 
@@ -240,7 +245,8 @@ Entity-selection methods accept **`EntityCollectionMethodOptions`**:
240
245
  Example:
241
246
 
242
247
  ```ts
243
- const stats = await quadro.Agency.agencyStats<MyStatsRow>([from, to])
248
+ const stats = await quadro.Agency.agencyStats<MyStatsRow, [string, string]>([from, to])
249
+ // or: agencyStats<MyStatsRow>([from, to]) when you only need to fix the result type
244
250
 
245
251
  const col = quadro.Reservation.all({ pageSize: 20 })
246
252
  // After the handle exists, e.g. from a hook or manual create:
@@ -253,6 +259,69 @@ If the catalog does not list a method, it will not appear on the client — rege
253
259
 
254
260
  ---
255
261
 
262
+ ## Narrowing function signatures (override files)
263
+
264
+ Generated ORDA **dataClass**, **entity**, and **entityCollection** methods, plus **datastore** methods, default to **loose** generics (`unknown` / `readonly unknown[]` for results and argument tuples). To type **return values** and **parameters** in TypeScript, add entries to the optional **override** modules beside the codegen output. The generator wires each method type as **`ResolveOverride<YourOverrides, "methodName", DefaultFn>`** in **`*.gen.ts`**: if your interface defines that **exact property name** (same spelling as the catalog / JS API), your signature wins; otherwise the default **`Quadro*Fn`** stands.
265
+
266
+ ### Where the files live
267
+
268
+ **Split typings (default):** for each exposed dataclass, **`entities/<ClassName>.overrides.ts`** declares up to three interfaces:
269
+
270
+ | Interface | Catalog `applyTo` | Example call site |
271
+ |-----------|-------------------|-------------------|
272
+ | **`<ClassName>DataclassOverrides`** | dataClass | `quadro.Car.findACar(...)` |
273
+ | **`<ClassName>EntityOverrides`** | entity | `(await quadro.Car.get(id)).isAvailable(...)` |
274
+ | **`<ClassName>EntityCollectionOverrides`** | entityCollection | On **`all()`** / **`query()`** handles and related **`list`** APIs |
275
+
276
+ **`datastore.overrides.ts`** (next to **`types.gen.ts`**) maps **top-level** datastore methods on **`quadro`** by **catalog method name**.
277
+
278
+ **Single `types.gen.ts` (**`--no-split-type-files`**):** same idea, with **`./<ClassName>.overrides.ts`** and **`./datastore.overrides.ts`** next to **`types.gen.ts`**.
279
+
280
+ Stub files may **re-export** **`QuadroDataClassFn`**, **`QuadroEntityFn`**, **`QuadroEntityCollectionFn`**, **`QuadroDatastoreFn`** from **`_quadroFns.gen.ts`**; you can **`import type`** those helpers from **`_quadroFns.gen.js`** (or `.ts`) when you add properties.
281
+
282
+ ### Fn type parameters (from `_quadroFns.gen.ts`)
283
+
284
+ - **`QuadroDataClassFn<R, A>`** — **`R`**: resolved **`Promise`** result type; **`A`**: **tuple** of positional arguments (sent as the JSON array body to 4D).
285
+ - **`QuadroEntityFn<R, A>`** — same shape for **per-entity** calls.
286
+ - **`QuadroEntityCollectionFn<R, A>`** — same **`R`** / **`A`**; real calls also accept **`init`** with **`EntityCollectionMethodOptions`** (`selection`, `entitySet`, …).
287
+ - **`QuadroDatastoreFn<B, R>`** — **`B`**: body type; **`R`**: result type for **`quadro.<datastoreMethod>(body?)`**.
288
+
289
+ ### Examples
290
+
291
+ **Per-class overrides** (`entities/Car.overrides.ts` in the split layout):
292
+
293
+ ```ts
294
+ import type { QuadroDataClassFn, QuadroEntityFn } from '../_quadroFns.gen.js'
295
+ import type { Car } from './Car.gen.js'
296
+
297
+ export interface CarDataclassOverrides {
298
+ /** Catalog method name must match exactly (e.g. findACar). */
299
+ findACar: QuadroDataClassFn<Car[], [string, string]>
300
+ }
301
+
302
+ export interface CarEntityOverrides {
303
+ isAvailable: QuadroEntityFn<boolean, readonly []>
304
+ }
305
+
306
+ export interface CarEntityCollectionOverrides {}
307
+ ```
308
+
309
+ **Datastore overrides** (`datastore.overrides.ts`):
310
+
311
+ ```ts
312
+ import type { QuadroDatastoreFn } from './_quadroFns.gen.js'
313
+
314
+ export interface DatastoreOverrides {
315
+ testFn: QuadroDatastoreFn<{ foo: string }, { ok: true }>
316
+ }
317
+ ```
318
+
319
+ ### Regenerate behavior
320
+
321
+ Run **`quadrokit-client generate`** after catalog changes. The tool **rewrites** **`*.gen.ts`** (and related generated files) but **keeps** your existing **`*.overrides.ts`** content when refreshing **`entities/`** (and only creates stub override files if missing). Do not rely on editing **`*.gen.ts`** — put signature narrowing in override modules only.
322
+
323
+ ---
324
+
256
325
  ## Low-level runtime (advanced)
257
326
 
258
327
  Import from **`@quadrokit/client/runtime`** when you need primitives without codegen:
package/dist/cli.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { catalogFetchInit, defaultLoginUrlFromCatalogUrl, fetch4DAdminSessionCookie, vLog, wrapFetchError, } from './generate/catalog-session.mjs';
5
6
  import { warnIfCatalogLacksAttributes, writeGenerated } from './generate/codegen.mjs';
@@ -13,6 +14,7 @@ function parseArgs(argv) {
13
14
  let out = '.quadrokit/generated';
14
15
  let verbose = false;
15
16
  let insecureTls = false;
17
+ let splitTypeFiles = true;
16
18
  for (let i = 0; i < argv.length; i++) {
17
19
  const a = argv[i];
18
20
  if (a === '--url' && argv[i + 1]) {
@@ -36,11 +38,14 @@ function parseArgs(argv) {
36
38
  else if (a === '--insecure-tls') {
37
39
  insecureTls = true;
38
40
  }
41
+ else if (a === '--no-split-type-files') {
42
+ splitTypeFiles = false;
43
+ }
39
44
  else if (!a.startsWith('-') && !command) {
40
45
  command = a;
41
46
  }
42
47
  }
43
- return { command, url, token, accessKey, loginUrl, out, verbose, insecureTls };
48
+ return { command, url, token, accessKey, loginUrl, out, verbose, insecureTls, splitTypeFiles };
44
49
  }
45
50
  /** 4D sometimes returns `dataClass` (summary) vs `dataClasses` (detail); align to `dataClasses`. */
46
51
  function normalizeCatalogJson(raw) {
@@ -64,7 +69,15 @@ async function loadCatalog(url, auth, debug) {
64
69
  vLog(debug, '🦙', 'QuadroKit catalog generate', 'Loading catalog…');
65
70
  vLog(debug, '📍', 'Catalog URL', url);
66
71
  if (url.startsWith('file:')) {
67
- const p = fileURLToPath(url);
72
+ let p;
73
+ try {
74
+ p = fileURLToPath(new URL(url));
75
+ }
76
+ catch {
77
+ // e.g. `file://../../assets/catalog.json` (invalid file URL host on some platforms)
78
+ const rest = url.replace(/^file:/i, '').replace(/^\/+/, '');
79
+ p = path.resolve(process.cwd(), rest);
80
+ }
68
81
  vLog(debug, '📂', 'Local file', p);
69
82
  const text = await readFile(p, 'utf8');
70
83
  vLog(debug, '✅', 'Catalog JSON', `Read ${text.length} bytes from disk`);
@@ -128,6 +141,10 @@ Debug / TLS:
128
141
  -v, --verbose Friendly step-by-step logs (emojis) on stderr
129
142
  --insecure-tls Skip TLS certificate verification (dev/self-signed HTTPS only)
130
143
 
144
+ Output layout (default: split entity + datastore typings):
145
+ --no-split-type-files Emit a single types.gen.ts (legacy); default is split files:
146
+ entities/<Class>.gen.ts, optional datastore.gen.ts, types.gen.ts barrel.
147
+
131
148
  Env / .env: VITE_4D_ORIGIN, QUADROKIT_ACCESS_KEY, QUADROKIT_LOGIN_URL, QUADROKIT_CATALOG_TOKEN,
132
149
  QUADROKIT_GENERATE_VERBOSE, QUADROKIT_INSECURE_TLS (set to 1 / true / yes)
133
150
 
@@ -157,7 +174,7 @@ Examples:
157
174
  token: accessKey ? undefined : bearer,
158
175
  }, debug);
159
176
  warnIfCatalogLacksAttributes(catalog);
160
- await writeGenerated(out, catalog);
177
+ await writeGenerated(out, catalog, { splitTypeFiles: parsed.splitTypeFiles });
161
178
  if (debug.verbose) {
162
179
  vLog(debug, '🎉', 'All set', `Generated client → ${out}`);
163
180
  }
@@ -16,8 +16,19 @@ export declare function buildCatalogRuntimeSpec(catalog: CatalogJson): {
16
16
  }[];
17
17
  keyNamesByClass: Record<string, string[]>;
18
18
  hasAuthentify: boolean;
19
+ datastoreMethodNames: string[];
19
20
  };
20
21
  /** Warn when the catalog lists dataclasses but omits attributes (e.g. plain `GET /rest/$catalog` instead of `/rest/$catalog/$all`). */
21
22
  export declare function warnIfCatalogLacksAttributes(catalog: CatalogJson): void;
22
- export declare function writeGenerated(outDir: string, catalog: CatalogJson): Promise<void>;
23
+ export interface WriteGeneratedOptions {
24
+ /**
25
+ * When `true` (default), emit each exposed dataclass as `entities/<Name>.gen.ts`, optional
26
+ * `datastore.gen.ts` (per-method `Datastore_*` aliases + `QuadroDatastoreMethodFn`), and a barrel `types.gen.ts`.
27
+ * When `false`, emit a single `types.gen.ts` (legacy layout).
28
+ * `entities/<Name>.overrides.ts` and `datastore.overrides.ts` are created only if missing; existing
29
+ * entity override files are preserved when the `entities/` folder is regenerated.
30
+ */
31
+ splitTypeFiles?: boolean;
32
+ }
33
+ export declare function writeGenerated(outDir: string, catalog: CatalogJson, options?: WriteGeneratedOptions): Promise<void>;
23
34
  //# sourceMappingURL=codegen.d.ts.map
@@ -1 +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;AA4GxF,yFAAyF;AACzF,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,WAAW,GAAG;IAC7D,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAA;QACZ,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QACnC,QAAQ,EAAE,MAAM,EAAE,CAAA;QAClB,iBAAiB,EAAE,MAAM,EAAE,CAAA;QAC3B,2BAA2B,EAAE,MAAM,EAAE,CAAA;QACrC,oBAAoB,EAAE,MAAM,EAAE,CAAA;QAC9B,SAAS,EAAE;YACT,IAAI,EAAE,MAAM,CAAA;YACZ,WAAW,EAAE,MAAM,CAAA;YACnB,2BAA2B,EAAE,MAAM,EAAE,CAAA;SACtC,EAAE,CAAA;KACJ,EAAE,CAAA;IACH,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IACzC,aAAa,EAAE,OAAO,CAAA;CACvB,CAuBA;AA6GD,uIAAuI;AACvI,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAYvE;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBxF"}
1
+ {"version":3,"file":"codegen.d.ts","sourceRoot":"","sources":["../../src/generate/codegen.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAGV,WAAW,EAEZ,MAAM,mBAAmB,CAAA;AAmd1B,yFAAyF;AACzF,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,WAAW,GAAG;IAC7D,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAA;QACZ,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QACnC,QAAQ,EAAE,MAAM,EAAE,CAAA;QAClB,iBAAiB,EAAE,MAAM,EAAE,CAAA;QAC3B,2BAA2B,EAAE,MAAM,EAAE,CAAA;QACrC,oBAAoB,EAAE,MAAM,EAAE,CAAA;QAC9B,SAAS,EAAE;YACT,IAAI,EAAE,MAAM,CAAA;YACZ,WAAW,EAAE,MAAM,CAAA;YACnB,2BAA2B,EAAE,MAAM,EAAE,CAAA;SACtC,EAAE,CAAA;KACJ,EAAE,CAAA;IACH,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IACzC,aAAa,EAAE,OAAO,CAAA;IACtB,oBAAoB,EAAE,MAAM,EAAE,CAAA;CAC/B,CAwBA;AAoOD,uIAAuI;AACvI,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAYvE;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAqCD,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,WAAW,EACpB,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,IAAI,CAAC,CA2Ff"}
@@ -1,6 +1,11 @@
1
- import { mkdir, writeFile } from 'node:fs/promises';
1
+ import { mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { sessionCookieName } from '@quadrokit/shared';
4
+ /**
5
+ * Suffix for import specifiers in generated `.ts` (`./Foo.gen.js`).
6
+ * Build with template + this const so `rename-dist-js-to-mjs` does not rewrite emitted paths in `dist/` to `.mjs`.
7
+ */
8
+ const EMITTED_TS_IMPORT_SUFFIX = '.js';
4
9
  function map4dType(attr) {
5
10
  const t = attr.type ?? 'unknown';
6
11
  switch (t) {
@@ -83,19 +88,315 @@ function emitInterface(dc) {
83
88
  lines.push('}');
84
89
  return lines.join('\n');
85
90
  }
91
+ /** Class names referenced as `relatedEntity` types (for per-entity file imports). Excludes self-references. */
92
+ function referencedEntityTypes(dc) {
93
+ const names = new Set();
94
+ for (const a of dc.attributes ?? []) {
95
+ if (a.kind === 'relatedEntity' && a.type && a.type !== dc.name) {
96
+ names.add(a.type);
97
+ }
98
+ }
99
+ return [...names].sort((x, y) => x.localeCompare(y));
100
+ }
101
+ function entityGenNamespaceImportName(className) {
102
+ return `Gen${className}`;
103
+ }
104
+ function emitQuadroFnTypesFile() {
105
+ return `/* eslint-disable */
106
+ /* Auto-generated by @quadrokit/client — do not edit */
107
+
108
+ import type { ClassFunctionHttpOptions, EntityCollectionMethodOptions } from '@quadrokit/client/runtime';
109
+
110
+ /** When the matching \`*Overrides\` interface defines a key, that signature wins; otherwise \`D\`. */
111
+ export type ResolveOverride<O extends object, K extends string, D> = K extends keyof O
112
+ ? O[Extract<K, keyof O>] extends infer V
113
+ ? [V] extends [never]
114
+ ? D
115
+ : V extends undefined
116
+ ? D
117
+ : V
118
+ : D
119
+ : D;
120
+
121
+ export type QuadroDataClassFn = <
122
+ R = unknown,
123
+ A extends readonly unknown[] = readonly unknown[],
124
+ >(
125
+ args?: A,
126
+ init?: ClassFunctionHttpOptions & { unwrapResult?: boolean },
127
+ ) => Promise<R>;
128
+
129
+ export type QuadroEntityFn = <
130
+ R = unknown,
131
+ A extends readonly unknown[] = readonly unknown[],
132
+ >(
133
+ args?: A,
134
+ init?: ClassFunctionHttpOptions & { unwrapResult?: boolean },
135
+ ) => Promise<R>;
136
+
137
+ export type QuadroEntityCollectionFn = <
138
+ R = unknown,
139
+ A extends readonly unknown[] = readonly unknown[],
140
+ >(
141
+ args?: A,
142
+ init?: EntityCollectionMethodOptions,
143
+ ) => Promise<R>;
144
+
145
+ export type QuadroDatastoreFn = <
146
+ B = unknown,
147
+ R = unknown,
148
+ >(
149
+ body?: B,
150
+ init?: { method?: 'GET' | 'POST' },
151
+ ) => Promise<R>;
152
+ `;
153
+ }
154
+ function emitOverridesStubFile(className, quadroFnRelativeToStub) {
155
+ return `/* eslint-disable */
156
+ /**
157
+ * Optional: narrow REST method typings for ${className}.
158
+ * Add entries under the interfaces (e.g. findACar: QuadroDataClassFn<Car[], [string, string]> — R then A).
159
+ * Import fn types from \`'${quadroFnRelativeToStub}'\` when you add entries (or reference re-exports below).
160
+ */
161
+ export type {
162
+ QuadroDataClassFn,
163
+ QuadroEntityFn,
164
+ QuadroEntityCollectionFn,
165
+ } from '${quadroFnRelativeToStub}';
166
+
167
+ export interface ${className}DataclassOverrides {}
168
+
169
+ export interface ${className}EntityOverrides {}
170
+
171
+ export interface ${className}EntityCollectionOverrides {}
172
+ `;
173
+ }
174
+ function emitDatastoreOverridesStub(quadroFnsRelativeToStub) {
175
+ return `/* eslint-disable */
176
+ /**
177
+ * Optional: narrow top-level datastore REST methods on the generated client (e.g. \`testFn\`).
178
+ * Add one property per catalog method name on \`DatastoreOverrides\`.
179
+ * Import \`QuadroDatastoreFn\` from \`'${quadroFnsRelativeToStub}'\` when you add entries.
180
+ */
181
+ export type { QuadroDatastoreFn } from '${quadroFnsRelativeToStub}';
182
+
183
+ export interface DatastoreOverrides {}
184
+ `;
185
+ }
186
+ /** Import names from \`_quadroFns.gen\` required for this class’s emitted method aliases (avoids unused imports under \`noUnusedLocals\`). */
187
+ function quadroFnTypeNamesForClass(dc) {
188
+ const dcMs = methodsForApply(dc, 'dataClass');
189
+ const emMs = methodsForApply(dc, 'entity');
190
+ const ecMs = methodsForApply(dc, 'entityCollection');
191
+ const out = [];
192
+ if (dcMs.length > 0 || emMs.length > 0 || ecMs.length > 0) {
193
+ out.push('ResolveOverride');
194
+ }
195
+ if (dcMs.length > 0) {
196
+ out.push('QuadroDataClassFn');
197
+ }
198
+ if (emMs.length > 0) {
199
+ out.push('QuadroEntityFn');
200
+ }
201
+ if (ecMs.length > 0) {
202
+ out.push('QuadroEntityCollectionFn');
203
+ }
204
+ return out;
205
+ }
206
+ function quadroFnTypeNamesForCatalog(classes, dsNames) {
207
+ const set = new Set();
208
+ for (const c of classes) {
209
+ for (const n of quadroFnTypeNamesForClass(c)) {
210
+ set.add(n);
211
+ }
212
+ }
213
+ if (dsNames.length > 0) {
214
+ set.add('ResolveOverride');
215
+ set.add('QuadroDatastoreFn');
216
+ }
217
+ const order = [
218
+ 'ResolveOverride',
219
+ 'QuadroDataClassFn',
220
+ 'QuadroEntityFn',
221
+ 'QuadroEntityCollectionFn',
222
+ 'QuadroDatastoreFn',
223
+ ];
224
+ return order.filter((n) => set.has(n));
225
+ }
226
+ function overrideTypeNamesForClass(c) {
227
+ const parts = [];
228
+ if (methodsForApply(c, 'dataClass').length > 0) {
229
+ parts.push(`${c.name}DataclassOverrides`);
230
+ }
231
+ if (methodsForApply(c, 'entity').length > 0) {
232
+ parts.push(`${c.name}EntityOverrides`);
233
+ }
234
+ if (methodsForApply(c, 'entityCollection').length > 0) {
235
+ parts.push(`${c.name}EntityCollectionOverrides`);
236
+ }
237
+ return parts;
238
+ }
239
+ function methodNameLiteralForKey(name) {
240
+ return JSON.stringify(name);
241
+ }
242
+ function buildMethodAliasLines(dc) {
243
+ const dcMs = methodsForApply(dc, 'dataClass');
244
+ const emMs = methodsForApply(dc, 'entity');
245
+ const ecMs = methodsForApply(dc, 'entityCollection');
246
+ const dataclassLines = dcMs.map((m) => {
247
+ const id = sanitizeMethodId(m.name);
248
+ const key = methodNameLiteralForKey(m.name);
249
+ return `export type ${dc.name}_dataclass_${id} = ResolveOverride<${dc.name}DataclassOverrides, ${key}, QuadroDataClassFn>`;
250
+ });
251
+ const entityLines = emMs.map((m) => {
252
+ const id = sanitizeMethodId(m.name);
253
+ const key = methodNameLiteralForKey(m.name);
254
+ return `export type ${dc.name}_entity_${id} = ResolveOverride<${dc.name}EntityOverrides, ${key}, QuadroEntityFn>`;
255
+ });
256
+ const ecLines = ecMs.map((m) => {
257
+ const id = sanitizeMethodId(m.name);
258
+ const key = methodNameLiteralForKey(m.name);
259
+ return `export type ${dc.name}_entityCollection_${id} = ResolveOverride<${dc.name}EntityCollectionOverrides, ${key}, QuadroEntityCollectionFn>`;
260
+ });
261
+ const entityMap = emMs.length > 0
262
+ ? `export type ${dc.name}EntityMethodMap = {
263
+ ${emMs
264
+ .map((m) => ` ${objectPropertyKey(m.name)}: ${dc.name}_entity_${sanitizeMethodId(m.name)};`)
265
+ .join('\n')}
266
+ };
267
+
268
+ export type ${dc.name}WithEntityMethods = ${dc.name} & Partial<${dc.name}EntityMethodMap>;
269
+ `
270
+ : '';
271
+ const ecMap = ecMs.length > 0
272
+ ? `export type ${dc.name}EntityCollectionMethodMap = {
273
+ ${ecMs
274
+ .map((m) => ` ${objectPropertyKey(m.name)}: ${dc.name}_entityCollection_${sanitizeMethodId(m.name)};`)
275
+ .join('\n')}
276
+ };
277
+ `
278
+ : '';
279
+ const methodBlock = [...dataclassLines, ...entityLines, ...ecLines].join('\n');
280
+ const tail = [entityMap, ecMap].filter(Boolean).join('\n');
281
+ return [methodBlock, tail].filter(Boolean).join('\n\n');
282
+ }
283
+ function emitEntityTypeBody(dc, opts) {
284
+ const refs = referencedEntityTypes(dc);
285
+ const fnNames = quadroFnTypeNamesForClass(dc);
286
+ const fnImport = fnNames.length > 0
287
+ ? `import type {\n ${fnNames.join(',\n ')},\n} from '${opts.quadroFnImport}';\n`
288
+ : '';
289
+ const ovNames = overrideTypeNamesForClass(dc);
290
+ const ovImport = ovNames.length > 0
291
+ ? `import type {\n ${ovNames.join(',\n ')},\n} from '${opts.overridesImport}';\n`
292
+ : '';
293
+ const runtimeImport = `import type { QuadroAttributePaths } from '@quadrokit/client/runtime';
294
+ `;
295
+ const refImports = refs.length > 0
296
+ ? `${refs
297
+ .map((r) => `import type { ${r} } from './${r}.gen${EMITTED_TS_IMPORT_SUFFIX}';`)
298
+ .join('\n')}\n`
299
+ : '';
300
+ const iface = emitInterface(dc);
301
+ const pathType = `export type ${dc.name}Path = QuadroAttributePaths<${dc.name}>;
302
+ `;
303
+ const methodAliases = buildMethodAliasLines(dc);
304
+ const blocks = [fnImport, ovImport, runtimeImport, refImports, iface, '', pathType, methodAliases]
305
+ .filter((x) => x !== '')
306
+ .join('\n');
307
+ return blocks;
308
+ }
309
+ function emitEntityTypeFile(dc) {
310
+ const header = `/* eslint-disable */\n/* Auto-generated by @quadrokit/client — do not edit */\n`;
311
+ const quadroPath = `../_quadroFns.gen${EMITTED_TS_IMPORT_SUFFIX}`;
312
+ const body = emitEntityTypeBody(dc, {
313
+ quadroFnImport: quadroPath,
314
+ overridesImport: `./${dc.name}.overrides${EMITTED_TS_IMPORT_SUFFIX}`,
315
+ });
316
+ return `${header}${body}\n`;
317
+ }
318
+ function buildDatastoreMethodAliasLines(catalog) {
319
+ const dsNames = datastoreMethodsExcludingAuthentify(catalog);
320
+ if (dsNames.length === 0) {
321
+ return '';
322
+ }
323
+ return dsNames
324
+ .map((n) => {
325
+ const id = sanitizeMethodId(n);
326
+ const key = methodNameLiteralForKey(n);
327
+ return `export type Datastore_${id} = ResolveOverride<DatastoreOverrides, ${key}, QuadroDatastoreFn>`;
328
+ })
329
+ .join('\n');
330
+ }
331
+ function emitDatastoreTypesFile(catalog) {
332
+ const dsNames = datastoreMethodsExcludingAuthentify(catalog);
333
+ const header = `/* eslint-disable */\n/* Auto-generated by @quadrokit/client — do not edit */\n\n`;
334
+ const imports = `import type {
335
+ ResolveOverride,
336
+ QuadroDatastoreFn,
337
+ } from './_quadroFns.gen${EMITTED_TS_IMPORT_SUFFIX}';
338
+
339
+ import type { DatastoreOverrides } from './datastore.overrides${EMITTED_TS_IMPORT_SUFFIX}';
340
+
341
+ `;
342
+ const base = `/** Default datastore method shape; per-method aliases below respect \`DatastoreOverrides\`. */
343
+ export type QuadroDatastoreMethodFn = QuadroDatastoreFn;
344
+
345
+ `;
346
+ const aliases = dsNames.length > 0 ? `${buildDatastoreMethodAliasLines(catalog)}\n` : '';
347
+ return `${header}${imports}${base}${aliases}`;
348
+ }
86
349
  /** Include dataclasses unless explicitly not exposed (`exposed === false`). Omitted `exposed` is treated as true. */
87
350
  function exposedDataClasses(catalog) {
88
351
  return catalog.dataClasses?.filter((d) => d.exposed !== false) ?? [];
89
352
  }
353
+ /** Normalized `applyTo` for catalog method rows (4D: `dataClass`, `entity`, `entityCollection`). */
354
+ const APPLY_TO_KIND = {
355
+ dataClass: 'dataclass',
356
+ entity: 'entity',
357
+ entityCollection: 'entitycollection',
358
+ };
359
+ function normalizeCatalogMethodApplyTo(applyTo) {
360
+ return applyTo?.trim().toLowerCase().replace(/\s+/g, '') ?? '';
361
+ }
90
362
  /** ORDA catalog methods by `applyTo` (4D: dataClass, entityCollection, entity). */
91
- function methodsForApply(dc, applyTo) {
363
+ function methodsForApply(dc, kind) {
92
364
  if (!dc) {
93
365
  return [];
94
366
  }
95
- return (dc.methods ?? []).filter((m) => m.applyTo === applyTo && m.exposed !== false);
367
+ const want = APPLY_TO_KIND[kind];
368
+ return (dc.methods ?? []).filter((m) => normalizeCatalogMethodApplyTo(m.applyTo) === want && m.exposed !== false);
369
+ }
370
+ function sanitizeMethodId(name) {
371
+ return name.replace(/[^\w$]/g, '_').replace(/_+/g, '_');
372
+ }
373
+ /** Top-level ORDA `catalog.methods` entry targeting the datastore (4D: `dataStore`; compare case-insensitively). */
374
+ function catalogMethodApplyToIsDatastore(applyTo) {
375
+ return applyTo?.trim().toLowerCase() === 'datastore';
96
376
  }
377
+ /**
378
+ * True when the catalog lists an exposed `authentify` datastore method (same rules as other datastore methods).
379
+ * Not emitted in types/runtime when absent or `exposed: false`.
380
+ */
97
381
  function catalogHasAuthentify(catalog) {
98
- return Boolean(catalog.methods?.some((m) => m.name === 'authentify' && m.applyTo === 'dataStore'));
382
+ return Boolean(catalog.methods?.some((m) => m.name?.toLowerCase() === 'authentify' &&
383
+ catalogMethodApplyToIsDatastore(m.applyTo) &&
384
+ m.exposed !== false));
385
+ }
386
+ /** Top-level ORDA `methods` with datastore `applyTo`, excluding `authentify` (typed as `authentify.login` when present). */
387
+ function datastoreMethodsExcludingAuthentify(catalog) {
388
+ return (catalog.methods ?? [])
389
+ .filter((m) => Boolean(m.name) &&
390
+ catalogMethodApplyToIsDatastore(m.applyTo) &&
391
+ m.exposed !== false &&
392
+ m.name.toLowerCase() !== 'authentify')
393
+ .map((m) => m.name);
394
+ }
395
+ function isValidJsIdentifier(name) {
396
+ return /^[A-Za-z_$][\w$]*$/.test(name);
397
+ }
398
+ function objectPropertyKey(name) {
399
+ return isValidJsIdentifier(name) ? name : JSON.stringify(name);
99
400
  }
100
401
  /** Serializable spec consumed by {@link buildQuadroClientFromCatalogSpec} at runtime. */
101
402
  export function buildCatalogRuntimeSpec(catalog) {
@@ -103,6 +404,7 @@ export function buildCatalogRuntimeSpec(catalog) {
103
404
  const byName = new Map(classes.map((c) => [c.name, c]));
104
405
  return {
105
406
  hasAuthentify: catalogHasAuthentify(catalog),
407
+ datastoreMethodNames: datastoreMethodsExcludingAuthentify(catalog),
106
408
  keyNamesByClass: Object.fromEntries(classes.map((x) => [x.name, [...keyNames(x)]])),
107
409
  classes: classes.map((c) => ({
108
410
  name: c.name,
@@ -119,62 +421,148 @@ export function buildCatalogRuntimeSpec(catalog) {
119
421
  })),
120
422
  };
121
423
  }
122
- function emitQuadroClientTypeBlock(catalog, withEntities) {
424
+ function emitQuadroClientTypeBlock(catalog, withEntities, datastoreFnStyle = 'inline',
425
+ /** When true, method types are referenced as `GenCar.Car_dataclass_*` from per-entity files. */
426
+ splitLayout = false) {
123
427
  const classes = exposedDataClasses(catalog);
124
428
  const hasAuthentify = catalogHasAuthentify(catalog);
429
+ const dsNames = datastoreMethodsExcludingAuthentify(catalog);
125
430
  const authLine = hasAuthentify
126
431
  ? ` authentify: { login: (body: { email: string; password: string }) => Promise<unknown> };\n`
127
432
  : '';
433
+ const dsProps = dsNames.length > 0
434
+ ? `${dsNames
435
+ .map((n) => {
436
+ const id = sanitizeMethodId(n);
437
+ const ref = datastoreFnStyle === 'imported' ? `GenDatastore.Datastore_${id}` : `Datastore_${id}`;
438
+ return ` ${objectPropertyKey(n)}: ${ref};`;
439
+ })
440
+ .join('\n')}\n`
441
+ : '';
128
442
  const tail = ` rpc: (segments: string[], init?: { method?: 'GET' | 'POST'; body?: unknown }) => Promise<unknown>;
129
443
  sessionCookieName: string;
130
444
  _http: QuadroHttp;
131
445
  };`;
132
446
  if (!withEntities || classes.length === 0) {
133
447
  return `export type QuadroClient = {
134
- ${authLine}${tail}
448
+ ${authLine}${dsProps}${tail}
135
449
  `;
136
450
  }
137
- const dcFn = `type QuadroDataClassFn = <R = unknown>(args?: readonly unknown[], init?: ClassFunctionHttpOptions & { unwrapResult?: boolean }) => Promise<R>;
138
-
139
- `;
140
451
  const branches = classes
141
452
  .map((c) => {
142
- const keys = methodsForApply(c, 'dataClass')
143
- .map((m) => `'${m.name}'`)
144
- .join(' | ');
145
- const rhs = keys
146
- ? `BaseDataClassApi<${c.name}, ${c.name}Path> & { [K in ${keys}]: QuadroDataClassFn }`
147
- : `BaseDataClassApi<${c.name}, ${c.name}Path>`;
148
- return ` ${c.name}: ${rhs};`;
453
+ const dcMs = methodsForApply(c, 'dataClass');
454
+ if (dcMs.length === 0) {
455
+ return ` ${c.name}: BaseDataClassApi<${c.name}, ${c.name}Path>;`;
456
+ }
457
+ const ns = splitLayout ? entityGenNamespaceImportName(c.name) : null;
458
+ const props = dcMs
459
+ .map((m) => {
460
+ const id = sanitizeMethodId(m.name);
461
+ const ref = ns ? `${ns}.${c.name}_dataclass_${id}` : `${c.name}_dataclass_${id}`;
462
+ return ` ${objectPropertyKey(m.name)}: ${ref};`;
463
+ })
464
+ .join('\n');
465
+ return ` ${c.name}: BaseDataClassApi<${c.name}, ${c.name}Path> & {
466
+ ${props}
467
+ };`;
149
468
  })
150
469
  .join('\n');
151
- return `${dcFn}export type QuadroClient = {
152
- ${authLine}${branches}
470
+ return `export type QuadroClient = {
471
+ ${authLine}${dsProps}${branches}
153
472
  ${tail}
154
473
  `;
155
474
  }
156
- function emitTypes(catalog) {
475
+ function emitTypesMonolithic(catalog) {
157
476
  const classes = exposedDataClasses(catalog);
477
+ const dsNames = datastoreMethodsExcludingAuthentify(catalog);
478
+ const quadroFnNames = quadroFnTypeNamesForCatalog(classes, dsNames);
479
+ const needsQuadroFns = quadroFnNames.length > 0;
158
480
  const interfaces = classes.map((c) => emitInterface(c));
159
481
  const pathTypes = classes.map((c) => {
160
482
  const name = `${c.name}Path`;
161
483
  return `export type ${name} = QuadroAttributePaths<${c.name}>;`;
162
484
  });
485
+ const methodAliasBlocks = classes.map((c) => buildMethodAliasLines(c)).join('\n\n');
486
+ const datastoreAliasBlock = dsNames.length > 0 ? `${buildDatastoreMethodAliasLines(catalog)}\n\n` : '';
163
487
  const header = `/* eslint-disable */\n/* Auto-generated by @quadrokit/client — do not edit */\n`;
164
488
  const pathNote = classes.length > 0
165
- ? `/**\n * *Path types use {@link QuadroAttributePaths} (recursive relation paths, depth-limited for circular graphs).\n * Override depth: \`type DeepEmployeePath = QuadroAttributePaths<Employee, 12>\`.\n */\n`
489
+ ? `/**\n * *Path types use {@link QuadroAttributePaths} (recursive relation paths, depth-limited for circular graphs).\n * Override depth: \`type DeepEmployeePath = QuadroAttributePaths<Employee, 12>\`.\n * Per-class REST method typings: \`*_dataclass_*\`, \`*_entity_*\`, \`*_entityCollection_*\`; narrow in \`./*.overrides.ts\`.\n * Datastore methods: \`Datastore_*\` aliases; narrow in \`./datastore.overrides.ts\`.\n */\n`
490
+ : dsNames.length > 0
491
+ ? `/**\n * Datastore REST methods: \`Datastore_*\` type aliases; narrow signatures in \`./datastore.overrides.ts\`.\n */\n`
492
+ : '';
493
+ const fnImport = needsQuadroFns
494
+ ? `import type {\n ${quadroFnNames.join(',\n ')},\n} from './_quadroFns.gen${EMITTED_TS_IMPORT_SUFFIX}';\n\n`
166
495
  : '';
496
+ const datastoreOverrideImport = dsNames.length > 0
497
+ ? `import type { DatastoreOverrides } from './datastore.overrides${EMITTED_TS_IMPORT_SUFFIX}';
498
+
499
+ `
500
+ : '';
501
+ const overrideImportLines = classes
502
+ .map((c) => {
503
+ const parts = overrideTypeNamesForClass(c);
504
+ if (parts.length === 0) {
505
+ return null;
506
+ }
507
+ return `import type { ${parts.join(', ')} } from './${c.name}.overrides${EMITTED_TS_IMPORT_SUFFIX}';`;
508
+ })
509
+ .filter(Boolean);
510
+ const overrideImports = overrideImportLines.length > 0 ? `${overrideImportLines.join('\n')}\n\n` : '';
167
511
  const runtimeImport = classes.length > 0
168
- ? `import type { BaseDataClassApi, ClassFunctionHttpOptions, QuadroAttributePaths, QuadroHttp } from '@quadrokit/client/runtime';\n\n`
512
+ ? `import type { BaseDataClassApi, QuadroAttributePaths, QuadroHttp } from '@quadrokit/client/runtime';\n\n`
169
513
  : `import type { QuadroHttp } from '@quadrokit/client/runtime';\n\n`;
514
+ if (classes.length === 0) {
515
+ const raw = catalog.dataClasses?.length ?? 0;
516
+ const note = raw > 0
517
+ ? `\n/**\n * No entity types: all ${raw} data class(es) in the catalog have exposed: false.\n * Enable REST exposure for tables in 4D, then run quadrokit-client generate again.\n */\n`
518
+ : dsNames.length > 0
519
+ ? ''
520
+ : `\n/**\n * No entity types: the catalog has no dataClasses (or an empty list).\n */\n`;
521
+ return `${header}${runtimeImport}${fnImport}${datastoreOverrideImport}${note}${pathNote}${datastoreAliasBlock}${emitQuadroClientTypeBlock(catalog, false, 'inline', false)}`;
522
+ }
523
+ return `${header}${runtimeImport}${fnImport}${overrideImports}${datastoreOverrideImport}${interfaces.join('\n\n')}\n\n${pathNote}${pathTypes.join('\n\n')}\n\n${datastoreAliasBlock}${methodAliasBlocks}\n\n${emitQuadroClientTypeBlock(catalog, true, 'inline', false)}`;
524
+ }
525
+ function emitTypesSplitMain(catalog) {
526
+ const classes = exposedDataClasses(catalog);
527
+ const dsNames = datastoreMethodsExcludingAuthentify(catalog);
528
+ const header = `/* eslint-disable */\n/* Auto-generated by @quadrokit/client — do not edit */\n`;
529
+ const pathNote = classes.length > 0
530
+ ? `/**\n * *Path types use {@link QuadroAttributePaths} (recursive relation paths, depth-limited for circular graphs).\n * Override depth: \`type DeepEmployeePath = QuadroAttributePaths<Employee, 12>\`.\n * Entity interfaces and REST method aliases live under \`./entities/*.gen.ts\`; optional signature overrides in \`./entities/*.overrides.ts\`.\n * Datastore: \`./datastore.gen.ts\` and \`./datastore.overrides.ts\` when present.\n */\n`
531
+ : dsNames.length > 0
532
+ ? `/**\n * Datastore method typings: \`./datastore.gen.ts\`; narrow signatures in \`./datastore.overrides.ts\`.\n */\n`
533
+ : '';
534
+ const datastoreImport = dsNames.length > 0
535
+ ? `import type * as GenDatastore from './datastore.gen${EMITTED_TS_IMPORT_SUFFIX}';\n\n`
536
+ : '';
537
+ const runtimeImport = classes.length > 0
538
+ ? `import type { BaseDataClassApi, QuadroHttp } from '@quadrokit/client/runtime';\n\n`
539
+ : `import type { QuadroHttp } from '@quadrokit/client/runtime';\n\n`;
540
+ const entityImports = classes.length > 0
541
+ ? `${classes
542
+ .map((c) => `import type { ${c.name}, ${c.name}Path } from './entities/${c.name}.gen${EMITTED_TS_IMPORT_SUFFIX}';`)
543
+ .join('\n')}\n\n`
544
+ : '';
545
+ const genNamespaceImports = classes.length > 0
546
+ ? `${classes
547
+ .filter((c) => methodsForApply(c, 'dataClass').length > 0)
548
+ .map((c) => `import type * as ${entityGenNamespaceImportName(c.name)} from './entities/${c.name}.gen${EMITTED_TS_IMPORT_SUFFIX}';`)
549
+ .join('\n')}${classes.some((c) => methodsForApply(c, 'dataClass').length > 0) ? '\n\n' : ''}`
550
+ : '';
551
+ const entityStarExports = classes.length > 0
552
+ ? `${classes
553
+ .map((c) => `export * from './entities/${c.name}.gen${EMITTED_TS_IMPORT_SUFFIX}';`)
554
+ .join('\n')}\n\n`
555
+ : '';
556
+ const datastoreStarExport = dsNames.length > 0 ? `export * from './datastore.gen${EMITTED_TS_IMPORT_SUFFIX}';\n\n` : '';
557
+ const datastoreStyle = dsNames.length > 0 ? 'imported' : 'inline';
170
558
  if (classes.length === 0) {
171
559
  const raw = catalog.dataClasses?.length ?? 0;
172
560
  const note = raw > 0
173
561
  ? `\n/**\n * No entity types: all ${raw} data class(es) in the catalog have exposed: false.\n * Enable REST exposure for tables in 4D, then run quadrokit-client generate again.\n */\n`
174
562
  : `\n/**\n * No entity types: the catalog has no dataClasses (or an empty list).\n */\n`;
175
- return `${header}${runtimeImport}${note}\n${emitQuadroClientTypeBlock(catalog, false)}`;
563
+ return `${header}${runtimeImport}${datastoreImport}${note}${pathNote}\n${datastoreStarExport}${emitQuadroClientTypeBlock(catalog, false, datastoreStyle, false)}`;
176
564
  }
177
- return `${header}${runtimeImport}${interfaces.join('\n\n')}\n\n${pathNote}${pathTypes.join('\n\n')}\n\n${emitQuadroClientTypeBlock(catalog, true)}`;
565
+ return `${header}${runtimeImport}${datastoreImport}${entityImports}${genNamespaceImports}${pathNote}${entityStarExports}${datastoreStarExport}\n${emitQuadroClientTypeBlock(catalog, true, datastoreStyle, true)}`;
178
566
  }
179
567
  function emitClient(catalog) {
180
568
  const dbName = catalog.__NAME ?? 'default';
@@ -226,10 +614,104 @@ export function warnIfCatalogLacksAttributes(catalog) {
226
614
  }
227
615
  console.error('quadrokit: catalog has dataclass names but no attribute details. Fetch the full catalog: GET /rest/$catalog/$all (plain /rest/$catalog omits attributes). See 4D REST docs: $catalog/$all.');
228
616
  }
229
- export async function writeGenerated(outDir, catalog) {
617
+ /** Writes `contents` only when `filePath` does not exist (used for `*.overrides.ts` stubs). */
618
+ async function writeStubIfMissing(filePath, contents) {
619
+ try {
620
+ await stat(filePath);
621
+ }
622
+ catch {
623
+ await writeFile(filePath, contents, 'utf8');
624
+ }
625
+ }
626
+ /** Reads existing `entities/*.overrides.ts` before the entities dir is recreated so user edits are kept. */
627
+ async function readExistingEntityOverrides(entitiesDir) {
628
+ const map = new Map();
629
+ try {
630
+ const st = await stat(entitiesDir);
631
+ if (!st.isDirectory()) {
632
+ return map;
633
+ }
634
+ const names = await readdir(entitiesDir);
635
+ for (const name of names) {
636
+ if (!name.endsWith('.overrides.ts')) {
637
+ continue;
638
+ }
639
+ const p = path.join(entitiesDir, name);
640
+ const s = await stat(p);
641
+ if (!s.isFile()) {
642
+ continue;
643
+ }
644
+ map.set(name, await readFile(p, 'utf8'));
645
+ }
646
+ }
647
+ catch {
648
+ // entities dir missing or unreadable
649
+ }
650
+ return map;
651
+ }
652
+ export async function writeGenerated(outDir, catalog, options = {}) {
653
+ const splitTypeFiles = options.splitTypeFiles !== false;
230
654
  await mkdir(outDir, { recursive: true });
655
+ const entitiesDir = path.join(outDir, 'entities');
656
+ const datastorePath = path.join(outDir, 'datastore.gen.ts');
657
+ const quadroFnsPath = path.join(outDir, '_quadroFns.gen.ts');
658
+ const classes = exposedDataClasses(catalog);
659
+ const dsNames = datastoreMethodsExcludingAuthentify(catalog);
660
+ if (splitTypeFiles) {
661
+ const preservedEntityOverrides = await readExistingEntityOverrides(entitiesDir);
662
+ await rm(entitiesDir, { recursive: true, force: true });
663
+ const needsQuadroFns = classes.length > 0 || dsNames.length > 0;
664
+ if (needsQuadroFns) {
665
+ await writeFile(quadroFnsPath, emitQuadroFnTypesFile(), 'utf8');
666
+ }
667
+ else {
668
+ await rm(quadroFnsPath, { force: true }).catch(() => { });
669
+ }
670
+ if (classes.length > 0) {
671
+ await mkdir(entitiesDir, { recursive: true });
672
+ for (const c of classes) {
673
+ await writeFile(path.join(entitiesDir, `${c.name}.gen.ts`), emitEntityTypeFile(c), 'utf8');
674
+ const overrideName = `${c.name}.overrides.ts`;
675
+ const overridePath = path.join(entitiesDir, overrideName);
676
+ const previous = preservedEntityOverrides.get(overrideName);
677
+ if (previous !== undefined) {
678
+ await writeFile(overridePath, previous, 'utf8');
679
+ }
680
+ else {
681
+ await writeStubIfMissing(overridePath, emitOverridesStubFile(c.name, `../_quadroFns.gen${EMITTED_TS_IMPORT_SUFFIX}`));
682
+ }
683
+ }
684
+ }
685
+ if (dsNames.length > 0) {
686
+ await writeFile(datastorePath, emitDatastoreTypesFile(catalog), 'utf8');
687
+ await writeStubIfMissing(path.join(outDir, 'datastore.overrides.ts'), emitDatastoreOverridesStub(`./_quadroFns.gen${EMITTED_TS_IMPORT_SUFFIX}`));
688
+ }
689
+ else {
690
+ await rm(datastorePath, { force: true }).catch(() => { });
691
+ }
692
+ await writeFile(path.join(outDir, 'types.gen.ts'), emitTypesSplitMain(catalog), 'utf8');
693
+ }
694
+ else {
695
+ await rm(entitiesDir, { recursive: true, force: true });
696
+ await rm(datastorePath, { force: true }).catch(() => { });
697
+ const needsQuadroFns = classes.length > 0 || dsNames.length > 0;
698
+ if (needsQuadroFns) {
699
+ await writeFile(quadroFnsPath, emitQuadroFnTypesFile(), 'utf8');
700
+ }
701
+ else {
702
+ await rm(quadroFnsPath, { force: true }).catch(() => { });
703
+ }
704
+ if (classes.length > 0) {
705
+ for (const c of classes) {
706
+ await writeStubIfMissing(path.join(outDir, `${c.name}.overrides.ts`), emitOverridesStubFile(c.name, `./_quadroFns.gen${EMITTED_TS_IMPORT_SUFFIX}`));
707
+ }
708
+ }
709
+ if (dsNames.length > 0) {
710
+ await writeStubIfMissing(path.join(outDir, 'datastore.overrides.ts'), emitDatastoreOverridesStub(`./_quadroFns.gen${EMITTED_TS_IMPORT_SUFFIX}`));
711
+ }
712
+ await writeFile(path.join(outDir, 'types.gen.ts'), emitTypesMonolithic(catalog), 'utf8');
713
+ }
231
714
  await writeFile(path.join(outDir, 'catalog.gen.json'), `${JSON.stringify(buildCatalogRuntimeSpec(catalog), null, 2)}\n`, 'utf8');
232
- await writeFile(path.join(outDir, 'types.gen.ts'), emitTypes(catalog), 'utf8');
233
715
  await writeFile(path.join(outDir, 'client.gen.ts'), emitClient(catalog), 'utf8');
234
716
  await writeFile(path.join(outDir, 'meta.json'), JSON.stringify({
235
717
  __NAME: catalog.__NAME,
@@ -13,7 +13,10 @@ export interface CatalogClassRuntimeSpec {
13
13
  export interface CatalogRuntimeSpec {
14
14
  classes: readonly CatalogClassRuntimeSpec[];
15
15
  keyNamesByClass: Readonly<Record<string, readonly string[]>>;
16
- hasAuthentify: boolean;
16
+ /** Set from the catalog at generate time; omit or `false` when `authentify` is not an exposed datastore method. */
17
+ hasAuthentify?: boolean;
18
+ /** Top-level datastore REST methods from `catalog.methods` (`applyTo: dataStore`), excluding `authentify`. */
19
+ datastoreMethodNames: readonly string[];
17
20
  }
18
21
  /**
19
22
  * Builds the object returned by generated `createClient()` from `catalog.gen.json`.
@@ -1 +1 @@
1
- {"version":3,"file":"catalog-builder.d.ts","sourceRoot":"","sources":["../../src/runtime/catalog-builder.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,qBAAqB,EAC3B,MAAM,iBAAiB,CAAA;AAExB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C,kEAAkE;AAClE,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,2BAA2B,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9C,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAA;IACvC,SAAS,EAAE,SAAS,qBAAqB,EAAE,CAAA;CAC5C;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,SAAS,uBAAuB,EAAE,CAAA;IAC3C,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAA;IAC5D,aAAa,EAAE,OAAO,CAAA;CACvB;AAqHD;;;GAGG;AACH,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,kBAAkB,EACxB,IAAI,EAAE;IAAE,iBAAiB,EAAE,MAAM,CAAA;CAAE,GAClC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAoBzB"}
1
+ {"version":3,"file":"catalog-builder.d.ts","sourceRoot":"","sources":["../../src/runtime/catalog-builder.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,qBAAqB,EAC3B,MAAM,iBAAiB,CAAA;AAExB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C,kEAAkE;AAClE,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,2BAA2B,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9C,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAA;IACvC,SAAS,EAAE,SAAS,qBAAqB,EAAE,CAAA;CAC5C;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,SAAS,uBAAuB,EAAE,CAAA;IAC3C,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAA;IAC5D,mHAAmH;IACnH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,8GAA8G;IAC9G,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAA;CACxC;AAqHD;;;GAGG;AACH,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,kBAAkB,EACxB,IAAI,EAAE;IAAE,iBAAiB,EAAE,MAAM,CAAA;CAAE,GAClC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA8BzB"}
@@ -84,11 +84,14 @@ function makeDataClassNamespace(http, c, keyNamesRecord) {
84
84
  export function buildQuadroClientFromCatalogSpec(http, spec, meta) {
85
85
  const keyNamesRecord = spec.keyNamesByClass;
86
86
  const out = {};
87
- if (spec.hasAuthentify) {
87
+ if (spec.hasAuthentify === true) {
88
88
  out.authentify = {
89
89
  login: (body) => callDatastorePath(http, ['authentify', 'login'], { body }, { operation: 'auth.login' }),
90
90
  };
91
91
  }
92
+ for (const name of spec.datastoreMethodNames ?? []) {
93
+ out[name] = (body, init) => callDatastorePath(http, [name], { body, method: init?.method }, { operation: 'datastore', methodName: name });
94
+ }
92
95
  for (const c of spec.classes) {
93
96
  out[c.name] = makeDataClassNamespace(http, c, keyNamesRecord);
94
97
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quadrokit/client",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "description": "Typed 4D REST client and catalog code generator for QuadroKit",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -33,7 +33,7 @@
33
33
  "generate:fixture": "bun run src/cli.ts generate --url file://../../assets/catalog.json --out ../../.quadrokit/generated-demo"
34
34
  },
35
35
  "dependencies": {
36
- "@quadrokit/shared": "^0.3.14",
36
+ "@quadrokit/shared": "^0.3.16",
37
37
  "undici": "^6.21.0"
38
38
  },
39
39
  "peerDependencies": {