@quadrokit/client 0.2.13 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,47 +1,219 @@
1
1
  # `@quadrokit/client`
2
2
 
3
- Typed **4D REST** helper: small runtime (`fetch` with `credentials: 'include'`, paging, [`$attributes`](https://developer.4d.com/docs/REST/attributes)) plus a **code generator** that turns a catalog JSON into `.quadrokit/generated`.
3
+ Typed [**4D REST**](https://developer.4d.com/docs/REST/REST_requests) client: a small runtime (`fetch` with `credentials: 'include'`, lists, entity sets, [`$attributes`](https://developer.4d.com/docs/REST/attributes)) and a **code generator** that turns a catalog JSON into a typed `createClient` and entity types.
4
+
5
+ ---
4
6
 
5
7
  ## Install
6
8
 
7
9
  ```bash
10
+ npm add @quadrokit/client
11
+ # or
8
12
  bun add @quadrokit/client
9
13
  ```
10
14
 
11
- ## CLI: `quadrokit-client`
15
+ The package exposes:
16
+
17
+ - **`@quadrokit/client`** — runtime + types (and anything your generated code needs).
18
+ - **`@quadrokit/client/runtime`** — low-level helpers if you build custom integrations.
19
+
20
+ ---
21
+
22
+ ## Generate the client from your 4D catalog
12
23
 
13
- After install, the binary name is **`quadrokit-client`** (not the scoped package name):
24
+ Install adds the **`quadrokit-client`** binary (alias: **`quadrokit`**).
14
25
 
15
26
  ```bash
16
27
  bunx quadrokit-client generate \
17
28
  --url 'http://localhost:7080/rest/$catalog/$all' \
18
- --token YOUR_TOKEN \
19
29
  --out .quadrokit/generated
20
30
  ```
21
31
 
22
- - **`--url`**: HTTP(S) catalog URL or `file:///absolute/path/to/catalog.json`. Use **`/rest/$catalog/$all`** so the JSON includes dataclass **attributes**; plain `/rest/$catalog` only lists names (empty types). Defaults to `${VITE_4D_ORIGIN}/rest/$catalog/$all`.
23
- - **`--token`**: optional `Authorization: Bearer …` for protected catalog endpoints
24
- - **`--out`**: output directory (default `.quadrokit/generated`)
32
+ ### Why `$catalog/$all`?
33
+
34
+ Use a URL that returns the **full** catalog (dataclasses **with attributes**). Plain [`/rest/$catalog`](https://developer.4d.com/docs/REST/catalog) often lists tables only; without attribute metadata, generated `select` paths and typings are incomplete. The generator warns if attributes are missing.
35
+
36
+ ### CLI options
37
+
38
+ | Option | Description |
39
+ |--------|-------------|
40
+ | **`--url`** | Catalog URL, or `file:///absolute/path/to/catalog.json`. If omitted: `${VITE_4D_ORIGIN}/rest/$catalog/$all` (from env / `.env`), else `http://127.0.0.1:7080/rest/$catalog/$all`. |
41
+ | **`--out`** | Output directory (default: `.quadrokit/generated`). |
42
+ | **`--token`** | `Authorization: Bearer …` for protected catalog endpoints (generator HTTP only). |
43
+ | **`--access-key`** | Multipart `accessKey` to 4D’s login API; generator obtains `4DAdminSID` and uses it for the catalog request. Requires an `http(s)` `--url` unless you pass **`--login-url`**. |
44
+ | **`--login-url`** | Full login URL (default: `{catalog origin}/api/login`). |
45
+ | **`-v` / `--verbose`** | Step-by-step logs on stderr. |
46
+ | **`--insecure-tls`** | Skip TLS verification (dev / self-signed HTTPS only). |
47
+
48
+ 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`.
49
+
50
+ ### Generated files
51
+
52
+ | File | Purpose |
53
+ |------|---------|
54
+ | **`types.gen.ts`** | Entity interfaces and `*Path` unions for typed `select`. |
55
+ | **`client.gen.ts`** | `createClient(config)` — dataclass APIs, optional `authentify`, `rpc`, `quadrokitCatalogMeta`. |
56
+ | **`meta.json`** | `__NAME`, `sessionCookieName` hint for 4D session cookies. |
57
+
58
+ Point your app imports at the generated folder, for example:
59
+
60
+ ```ts
61
+ import { createClient } from './.quadrokit/generated/client.gen.js'
62
+ import type { Agency } from './.quadrokit/generated/types.gen.js'
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Creating the client
68
+
69
+ ```ts
70
+ import { createClient } from './.quadrokit/generated/client.gen.js'
71
+
72
+ const quadro = createClient({
73
+ /** Base URL of the REST root, e.g. `https://host:port/rest` or `/rest` behind a dev proxy */
74
+ baseURL: import.meta.env.VITE_4D_REST_URL ?? '/rest',
75
+ /** Optional: swap `fetch` (e.g. tests, Node with undici) */
76
+ fetchImpl: globalThis.fetch,
77
+ /** Optional: default headers on every request */
78
+ defaultHeaders: { Authorization: `Bearer ${token}` },
79
+ })
80
+ ```
81
+
82
+ At runtime, requests use **`credentials: 'include'`** so session cookies (e.g. `4DSID_<datastore>`) are sent when your app and 4D share the same site or proxy. See `meta.json` → `sessionCookieName` after generate.
83
+
84
+ ---
85
+
86
+ ## Dataclass API (generated)
87
+
88
+ For each exposed dataclass, the client exposes a namespace such as `quadro.Agency`, `quadro.Reservation`, etc.
89
+
90
+ ### `all(options?)` — full class as an async iterable “collection”
91
+
92
+ Walk entities with optional **`select`**, **`filter`**, **`orderby`**, paging, and 4D **entity sets** (`$method=entityset`) for efficient paging.
93
+
94
+ ```ts
95
+ const col = quadro.Agency.all({
96
+ select: ['ID', 'name', 'department.name'] as const,
97
+ filter: 'name = "Paris"',
98
+ page: 1,
99
+ pageSize: 50,
100
+ signal: ac.signal,
101
+ })
102
+
103
+ for await (const agency of col) {
104
+ console.log(agency.name)
105
+ }
106
+
107
+ await col.release() // release server entity set when done (see 4D $method=release)
108
+ ```
109
+
110
+ Important options (see `CollectionOptions` in `@quadrokit/client/runtime`):
25
111
 
26
- ## Generated output
112
+ | Option | Role |
113
+ |--------|------|
114
+ | **`select`** | [`$attributes`](https://developer.4d.com/docs/REST/attributes): scalar or relation paths (`'manager.name'`). |
115
+ | **`filter` / `orderby`** | OData-style list constraints. |
116
+ | **`page` / `pageSize`** | Client-side paging over the list / entity set. |
117
+ | **`maxItems`** | Stop iteration after N rows. |
118
+ | **`signal`** | `AbortController` — abort in-flight requests when unmounting or changing filters. |
119
+ | **`reuseEntitySet` / `onEntitySetReady`** | Reuse a cached entity-set URI across requests (see your app’s paging hooks). |
27
120
 
28
- - `types.gen.ts` — entity interfaces and `*Path` unions for `select`
29
- - `client.gen.ts` — `createClient({ baseURL })` with dataclasses, `authentify.login` when present in catalog, and `rpc()` escape hatch
30
- - `meta.json` — catalog `__NAME`, `sessionCookieName` hint
121
+ ### `query(filter, options?)`
31
122
 
32
- ## Runtime imports
123
+ Same shape as `all`, but with a **`filter`** string as the first argument (and optional `params` for `:1`, `:2`, … placeholders in the filter).
33
124
 
34
- Apps and generated code import low-level pieces from:
125
+ ### `get(id, options?)`
126
+
127
+ Load one entity by primary key; optional **`select`** for `$attributes`.
128
+
129
+ ### `delete(id)`
130
+
131
+ `DELETE` the entity.
132
+
133
+ ### Related entities on a row
134
+
135
+ Mapped rows get non-enumerable properties for **related collections** (e.g. `agency.todayBookings`) with a **`.list(options)`** that returns another collection handle for the child dataclass.
136
+
137
+ ---
138
+
139
+ ## ORDA class functions (generated from catalog)
140
+
141
+ If your 4D project exposes methods in the REST catalog, the generator wires them by `applyTo` (see [Calling class functions](https://developer.4d.com/docs/REST/classFunctions.html)):
142
+
143
+ | Catalog `applyTo` | Where it appears in JS | REST shape (simplified) |
144
+ |-------------------|-------------------------|-------------------------|
145
+ | **`dataClass`** | `quadro.Agency.agencyStats<R>(args, init?)` | `POST /rest/{Class}/{function}` with JSON array body |
146
+ | **`entityCollection`** | On **`all()` / `query()`** handles and on related `{ list, … }` APIs | `POST /rest/{Class}/{function}` + optional `selection`, `entitySet` |
147
+ | **`entity`** | On each **mapped entity** row | `POST /rest/{Class}({key})/{function}` |
148
+
149
+ - **`args`**: `readonly unknown[]` — 4D receives a **JSON array** of parameters (scalars, entities, entity selections per 4D rules).
150
+ - **`init`**: optional `{ method: 'GET' \| 'POST', unwrapResult?, signal?, … }` — GET uses `?$params=…` for [HTTP GET functions](https://developer.4d.com/docs/ORDA/ordaClasses#onhttpget-keyword).
151
+ - **`unwrapResult`**: when `true` (default), `{ "result": x }` responses are unwrapped to `x`.
152
+
153
+ Entity-selection methods accept **`EntityCollectionMethodOptions`**:
154
+
155
+ - **`selection`**: `{ filter?, orderby?, select?, page?, pageSize? }` → `$filter`, `$orderby`, `$attributes`, `$skip`, `$top`.
156
+ - **`entitySet`**: UUID or path segment so the URL includes `…/$entityset/{uuid}`.
157
+
158
+ Example:
35
159
 
36
160
  ```ts
37
- import { QuadroHttp, type SelectedEntity } from '@quadrokit/client/runtime';
161
+ const stats = await quadro.Agency.agencyStats<MyStatsRow>([from, to])
162
+
163
+ const col = quadro.Reservation.all({ pageSize: 20 })
164
+ // After the handle exists, e.g. from a hook or manual create:
165
+ await col.getFirst?.([], { selection: { filter: 'ID > 0', pageSize: 1 } })
166
+
167
+ await reservation.cancel<{ ok: boolean }>([], { signal: ac.signal })
38
168
  ```
39
169
 
40
- The main entry re-exports the runtime barrel.
170
+ If the catalog does not list a method, it will not appear on the client — regenerate after changing 4D exposure.
171
+
172
+ ---
41
173
 
42
- ## Dev in this monorepo
174
+ ## Low-level runtime (advanced)
175
+
176
+ Import from **`@quadrokit/client/runtime`** when you need primitives without codegen:
177
+
178
+ | Export | Use |
179
+ |--------|-----|
180
+ | **`QuadroHttp`** | Thin wrapper: `json()`, `void()`, `request()` with base URL + default headers. |
181
+ | **`createCollection`** | Build a `CollectionHandle` from a `CollectionContext` + options + row mapper. |
182
+ | **`callDataClassFunction`**, **`callEntityFunction`**, **`callEntityCollectionFunction`** | Call ORDA functions by name and path (same REST rules as above). |
183
+ | **`callDatastorePath`**, **`createClient`’s `rpc`** | Arbitrary segments under the REST root, e.g. datastore or singleton paths. |
184
+ | **`buildListSearchParams`**, **`buildMethodSelectionQuery`**, … | Query-string helpers aligned with 4D. |
185
+ | **`QuadroHttpError`** | Thrown on non-OK HTTP with status and body text. |
186
+
187
+ The generated `createClient` is the recommended surface; these are for tooling, tests, or custom wrappers.
188
+
189
+ ---
190
+
191
+ ## Errors
192
+
193
+ Failed HTTP responses throw **`QuadroHttpError`** (status + response body). Handle network errors separately (`fetch` failures).
194
+
195
+ ---
196
+
197
+ ## Monorepo development
198
+
199
+ From `packages/client`:
43
200
 
44
201
  ```bash
45
202
  bun run typecheck
46
- bun run src/cli.ts generate --url file://../../assets/catalog.json --out ../../.quadrokit/generated
203
+ bun run build
204
+ ```
205
+
206
+ Example generate from the fixture catalog (adjust `file://` to an absolute path on your machine):
207
+
208
+ ```bash
209
+ bun run src/cli.ts generate --url file:///ABS/path/to/assets/catalog.json --out ../../.quadrokit/generated-demo
47
210
  ```
211
+
212
+ ---
213
+
214
+ ## Further reading
215
+
216
+ - [4D REST requests](https://developer.4d.com/docs/REST/REST_requests)
217
+ - [REST `$attributes`](https://developer.4d.com/docs/REST/attributes)
218
+ - [REST `$method` (entity set, release, …)](https://developer.4d.com/docs/REST/method)
219
+ - [Calling class functions (ORDA)](https://developer.4d.com/docs/REST/classFunctions.html)
@@ -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;AA0UxF,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,CAiBxF"}
1
+ {"version":3,"file":"codegen.d.ts","sourceRoot":"","sources":["../../src/generate/codegen.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAsC,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAmXxF,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,CAiBxF"}
@@ -108,6 +108,13 @@ function emitInterface(dc, _byName) {
108
108
  function exposedDataClasses(catalog) {
109
109
  return catalog.dataClasses?.filter((d) => d.exposed !== false) ?? [];
110
110
  }
111
+ /** ORDA catalog methods by `applyTo` (4D: dataClass, entityCollection, entity). */
112
+ function methodsForApply(dc, applyTo) {
113
+ if (!dc) {
114
+ return [];
115
+ }
116
+ return (dc.methods ?? []).filter((m) => m.applyTo === applyTo && m.exposed !== false);
117
+ }
111
118
  function emitTypes(catalog) {
112
119
  const classes = exposedDataClasses(catalog);
113
120
  const byName = new Map(classes.map((c) => [c.name, c]));
@@ -130,16 +137,18 @@ function emitTypes(catalog) {
130
137
  }
131
138
  function emitClient(catalog) {
132
139
  const classes = exposedDataClasses(catalog);
140
+ const byName = new Map(classes.map((c) => [c.name, c]));
133
141
  const dbName = catalog.__NAME ?? 'default';
134
142
  const hasAuthentify = catalog.methods?.some((m) => m.name === 'authentify' && m.applyTo === 'dataStore');
135
143
  const keyNamesRecord = Object.fromEntries(classes.map((x) => [x.name, keyNames(x)]));
136
144
  const imports = classes.length > 0
137
- ? `/* 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`
145
+ ? `/* eslint-disable */\n/* Auto-generated by @quadrokit/client — do not edit */\n\nimport {\n QuadroHttp,\n makeDataClassApi,\n attachRelatedApis,\n attachEntityClassMethods,\n bindEntityCollectionMethods,\n callDataClassFunction,\n callDatastorePath,\n type ClassFunctionHttpOptions,\n type CollectionHandle,\n type CollectionOptions,\n type SelectedEntity,\n} from '@quadrokit/client/runtime';\n`
138
146
  : `/* eslint-disable */\n/* Auto-generated by @quadrokit/client — do not edit */\n\nimport { QuadroHttp, callDatastorePath } from '@quadrokit/client/runtime';\n`;
139
147
  const typeImports = classes.map((c) => c.name).join(', ');
140
148
  const pathImports = classes.map((c) => `${c.name}Path`).join(', ');
141
149
  const typeImportLine = typeImports || pathImports
142
- ? `\nimport type { ${[typeImports, pathImports].filter(Boolean).join(', ')} } from './types.gen.mjs';\n\n`
150
+ ? // Split so `rename-dist-js-to-mjs` does not rewrite this *emitted* import to `.mjs`.
151
+ `\nimport type { ${[typeImports, pathImports].filter(Boolean).join(', ')} } from './types.gen';\n\n`
143
152
  : '\n';
144
153
  const header = `${imports}${typeImportLine}`;
145
154
  const metaExport = `
@@ -159,9 +168,26 @@ export interface QuadroClientConfig {
159
168
  .map((c) => {
160
169
  const nav = navigableRelations(c);
161
170
  const relMap = JSON.stringify(relationTargets(c));
171
+ const kn = JSON.stringify(keyNames(c));
172
+ const ecNav = nav.map((n) => ({
173
+ attr: n.attr,
174
+ targetClass: n.targetClass,
175
+ entityCollectionMethodNames: methodsForApply(byName.get(n.targetClass), 'entityCollection').map((m) => m.name),
176
+ }));
177
+ const entityMethodNames = methodsForApply(c, 'entity').map((m) => m.name);
162
178
  return ` function map${c.name}Row(http: QuadroHttp, raw: unknown): ${c.name} {
163
179
  const row = raw as ${c.name};
164
180
  const pk = (row as { ID?: number }).ID ?? (row as { id?: number }).id;
181
+ attachEntityClassMethods(
182
+ raw,
183
+ {
184
+ http,
185
+ className: '${c.name}',
186
+ relationMap: ${relMap},
187
+ keyNames: ${kn},
188
+ },
189
+ ${JSON.stringify(entityMethodNames)},
190
+ );
165
191
  attachRelatedApis(
166
192
  raw,
167
193
  {
@@ -171,7 +197,7 @@ export interface QuadroClientConfig {
171
197
  relationMap: ${relMap},
172
198
  keyNames: ${JSON.stringify(keyNamesRecord)},
173
199
  },
174
- ${JSON.stringify(nav)},
200
+ ${JSON.stringify(ecNav)},
175
201
  );
176
202
  return row;
177
203
  }`;
@@ -182,6 +208,12 @@ export interface QuadroClientConfig {
182
208
  const relMap = JSON.stringify(relationTargets(c));
183
209
  const kn = JSON.stringify(keyNames(c));
184
210
  const pathsType = `${c.name}Path`;
211
+ const ecNames = JSON.stringify(methodsForApply(c, 'entityCollection').map((m) => m.name));
212
+ const dcBlock = methodsForApply(c, 'dataClass')
213
+ .map((m) => ` ${m.name}: async <R = unknown>(args: readonly unknown[] = [], init?: ClassFunctionHttpOptions & { unwrapResult?: boolean }) =>
214
+ callDataClassFunction<R>(http, '${c.name}', '${m.name}', args, init),`)
215
+ .join('\n');
216
+ const dcPrefix = dcBlock ? `${dcBlock}\n` : '';
185
217
  return ` ${c.name}: (() => {
186
218
  const cfg = {
187
219
  http,
@@ -191,10 +223,11 @@ export interface QuadroClientConfig {
191
223
  } as const;
192
224
  const api = makeDataClassApi<${c.name}>(cfg);
193
225
  return {
194
- all<S extends readonly ${pathsType}[] = readonly []>(
226
+ ${dcPrefix} all<S extends readonly ${pathsType}[] = readonly []>(
195
227
  options?: CollectionOptions & { select?: S },
196
228
  ): CollectionHandle<S['length'] extends 0 ? ${c.name} : SelectedEntity<${c.name}, S>> {
197
229
  const inner = api.all(options as CollectionOptions);
230
+ bindEntityCollectionMethods(inner, { http, className: '${c.name}', relationMap: ${relMap} }, ${ecNames});
198
231
  return {
199
232
  ...inner,
200
233
  delete: inner.delete.bind(inner),
@@ -238,6 +271,7 @@ export interface QuadroClientConfig {
238
271
  options?: CollectionOptions & { params?: unknown[]; select?: S },
239
272
  ): CollectionHandle<S['length'] extends 0 ? ${c.name} : SelectedEntity<${c.name}, S>> {
240
273
  const inner = api.query(filter, options as CollectionOptions);
274
+ bindEntityCollectionMethods(inner, { http, className: '${c.name}', relationMap: ${relMap} }, ${ecNames});
241
275
  return {
242
276
  ...inner,
243
277
  delete: inner.delete.bind(inner),
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ORDA class functions over REST ([4D docs](https://developer.4d.com/docs/REST/classFunctions.html)):
3
+ * dataclass, entity selection (entityCollection), and entity methods.
4
+ */
5
+ import type { QuadroHttp } from './http.js';
6
+ import { type MethodSelectionQuery } from './query.js';
7
+ export type ClassFunctionHttpOptions = {
8
+ /** POST (default) or GET when the function is declared with `onHTTPGet`. */
9
+ method?: 'GET' | 'POST';
10
+ signal?: AbortSignal;
11
+ };
12
+ /** Unwrap `{ "result": ... }` when present; otherwise return the body as-is (e.g. entity payloads). */
13
+ export declare function unwrapClassFunctionResult<T>(body: unknown): T;
14
+ /**
15
+ * Dataclass class function: `POST /rest/{dataClass}/{function}` with a JSON array body.
16
+ * GET: `?$params=[...]` ([docs](https://developer.4d.com/docs/REST/classFunctions.html)).
17
+ */
18
+ export declare function callDataClassFunction<T>(http: QuadroHttp, dataClass: string, functionName: string, args: readonly unknown[], options?: ClassFunctionHttpOptions & {
19
+ unwrapResult?: boolean;
20
+ }): Promise<T>;
21
+ /**
22
+ * Entity class function: `POST /rest/{dataClass}({key})/{function}`.
23
+ */
24
+ export declare function callEntityFunction<T>(http: QuadroHttp, dataClass: string, entityKey: string | number, functionName: string, args: readonly unknown[], options?: ClassFunctionHttpOptions & {
25
+ unwrapResult?: boolean;
26
+ }): Promise<T>;
27
+ export type EntityCollectionMethodOptions = ClassFunctionHttpOptions & {
28
+ unwrapResult?: boolean;
29
+ /**
30
+ * Restrict the entity selection (`$filter`, `$orderby`, `$attributes`, `$skip` / `$top`).
31
+ * ([docs](https://developer.4d.com/docs/REST/classFunctions.html)).
32
+ */
33
+ selection?: MethodSelectionQuery;
34
+ /**
35
+ * Existing server entity set: UUID or `.../$entityset/{uuid}` suffix.
36
+ */
37
+ entitySet?: string;
38
+ };
39
+ /**
40
+ * Entity selection class function: `POST /rest/{dataClass}/{function}` with optional
41
+ * `/$entityset/{uuid}` and list query (`$filter`, `$orderby`, …).
42
+ */
43
+ export declare function callEntityCollectionFunction<T>(http: QuadroHttp, dataClass: string, relationMap: Record<string, string>, functionName: string, args: readonly unknown[], options?: EntityCollectionMethodOptions): Promise<T>;
44
+ //# sourceMappingURL=class-function.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"class-function.d.ts","sourceRoot":"","sources":["../../src/runtime/class-function.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,EAA6B,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEjF,MAAM,MAAM,wBAAwB,GAAG;IACrC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB,CAAA;AAED,uGAAuG;AACvG,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,GAAG,CAAC,CAK7D;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAC3C,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,SAAS,OAAO,EAAE,EACxB,OAAO,CAAC,EAAE,wBAAwB,GAAG;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9D,OAAO,CAAC,CAAC,CAAC,CAoBZ;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EACxC,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAAG,MAAM,EAC1B,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,SAAS,OAAO,EAAE,EACxB,OAAO,CAAC,EAAE,wBAAwB,GAAG;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9D,OAAO,CAAC,CAAC,CAAC,CAqBZ;AAED,MAAM,MAAM,6BAA6B,GAAG,wBAAwB,GAAG;IACrE,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;;OAGG;IACH,SAAS,CAAC,EAAE,oBAAoB,CAAA;IAChC;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;GAGG;AACH,wBAAsB,4BAA4B,CAAC,CAAC,EAClD,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACnC,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,SAAS,OAAO,EAAE,EACxB,OAAO,CAAC,EAAE,6BAA6B,GACtC,OAAO,CAAC,CAAC,CAAC,CA2BZ"}
@@ -0,0 +1,93 @@
1
+ import { buildMethodSelectionQuery } from './query.mjs';
2
+ /** Unwrap `{ "result": ... }` when present; otherwise return the body as-is (e.g. entity payloads). */
3
+ export function unwrapClassFunctionResult(body) {
4
+ if (body !== null && typeof body === 'object' && 'result' in body) {
5
+ return body.result;
6
+ }
7
+ return body;
8
+ }
9
+ /**
10
+ * Dataclass class function: `POST /rest/{dataClass}/{function}` with a JSON array body.
11
+ * GET: `?$params=[...]` ([docs](https://developer.4d.com/docs/REST/classFunctions.html)).
12
+ */
13
+ export async function callDataClassFunction(http, dataClass, functionName, args, options) {
14
+ const method = options?.method ?? 'POST';
15
+ const unwrap = options?.unwrapResult !== false;
16
+ const base = `/${dataClass}/${functionName}`;
17
+ let body;
18
+ if (method === 'GET') {
19
+ const params = new URLSearchParams();
20
+ params.set('$params', JSON.stringify([...args]));
21
+ body = await http.json(`${base}?${params.toString()}`, {
22
+ method: 'GET',
23
+ signal: options?.signal,
24
+ });
25
+ }
26
+ else {
27
+ body = await http.json(base, {
28
+ method: 'POST',
29
+ body: JSON.stringify([...args]),
30
+ signal: options?.signal,
31
+ });
32
+ }
33
+ return (unwrap ? unwrapClassFunctionResult(body) : body);
34
+ }
35
+ /**
36
+ * Entity class function: `POST /rest/{dataClass}({key})/{function}`.
37
+ */
38
+ export async function callEntityFunction(http, dataClass, entityKey, functionName, args, options) {
39
+ const method = options?.method ?? 'POST';
40
+ const unwrap = options?.unwrapResult !== false;
41
+ const key = encodeURIComponent(String(entityKey));
42
+ const base = `/${dataClass}(${key})/${functionName}`;
43
+ let body;
44
+ if (method === 'GET') {
45
+ const params = new URLSearchParams();
46
+ params.set('$params', JSON.stringify([...args]));
47
+ body = await http.json(`${base}?${params.toString()}`, {
48
+ method: 'GET',
49
+ signal: options?.signal,
50
+ });
51
+ }
52
+ else {
53
+ body = await http.json(base, {
54
+ method: 'POST',
55
+ body: JSON.stringify([...args]),
56
+ signal: options?.signal,
57
+ });
58
+ }
59
+ return (unwrap ? unwrapClassFunctionResult(body) : body);
60
+ }
61
+ /**
62
+ * Entity selection class function: `POST /rest/{dataClass}/{function}` with optional
63
+ * `/$entityset/{uuid}` and list query (`$filter`, `$orderby`, …).
64
+ */
65
+ export async function callEntityCollectionFunction(http, dataClass, relationMap, functionName, args, options) {
66
+ const method = options?.method ?? 'POST';
67
+ const unwrap = options?.unwrapResult !== false;
68
+ let path = `/${dataClass}/${functionName}`;
69
+ if (options?.entitySet) {
70
+ const raw = options.entitySet.trim().replace(/^\//, '');
71
+ const id = raw.startsWith('$entityset') ? raw.replace(/^\$entityset\/?/, '') : raw;
72
+ path += `/$entityset/${encodeURIComponent(id)}`;
73
+ }
74
+ path += buildMethodSelectionQuery(dataClass, relationMap, options?.selection);
75
+ let body;
76
+ if (method === 'GET') {
77
+ const params = new URLSearchParams();
78
+ params.set('$params', JSON.stringify([...args]));
79
+ const join = path.includes('?') ? '&' : '?';
80
+ body = await http.json(`${path}${join}${params.toString()}`, {
81
+ method: 'GET',
82
+ signal: options?.signal,
83
+ });
84
+ }
85
+ else {
86
+ body = await http.json(path, {
87
+ method: 'POST',
88
+ body: JSON.stringify([...args]),
89
+ signal: options?.signal,
90
+ });
91
+ }
92
+ return (unwrap ? unwrapClassFunctionResult(body) : body);
93
+ }
@@ -1,4 +1,4 @@
1
- import type { QuadroHttp } from './http.js';
1
+ import { type QuadroHttp } from './http.js';
2
2
  import { type ListQueryParams } from './query.js';
3
3
  export interface CollectionContext {
4
4
  http: QuadroHttp;
@@ -18,6 +18,13 @@ export interface CollectionOptions extends ListQueryParams {
18
18
  maxItems?: number;
19
19
  /** When aborted (e.g. React effect cleanup), list fetches stop and iteration ends. */
20
20
  signal?: AbortSignal;
21
+ /**
22
+ * Skip `$top=0` entity-set creation and use this URI (e.g. from `__ENTITYSET` or
23
+ * {@link onEntitySetReady}). Path form must match what the client uses after {@link parseEntitySetUri}.
24
+ */
25
+ reuseEntitySet?: string;
26
+ /** Fired once when a new entity set is created (not when {@link reuseEntitySet} is used). */
27
+ onEntitySetReady?: (entitySetPath: string) => void;
21
28
  }
22
29
  export type MapRow<R> = (raw: unknown) => R;
23
30
  /**
@@ -1 +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;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sFAAsF;IACtF,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;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,CA+FrB;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"}
1
+ {"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../../src/runtime/collection.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0C,KAAK,UAAU,EAAE,MAAM,WAAW,CAAA;AACnF,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,YAAY,CAAA;AAGnB,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;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sFAAsF;IACtF,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,6FAA6F;IAC7F,gBAAgB,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,IAAI,CAAA;CACnD;AAED,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,KAAK,CAAC,CAAA;AAsE3C;;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,CAmKrB;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"}
@@ -1,5 +1,56 @@
1
- import { buildListSearchParams } from './query.mjs';
1
+ import { mountPathFromBaseURL, normalizeBaseURL } from './http.mjs';
2
+ import { buildEntitySetCreationParams, buildEntitySetPageParams, buildListSearchParams, } from './query.mjs';
2
3
  import { unwrapEntityList } from './unwrap.mjs';
4
+ /**
5
+ * If `path` still starts with the same pathname as `baseURL` (e.g. `/rest`), strip it.
6
+ * Avoids `baseURL` + path → `.../rest/rest/Agency/...` when `__ENTITYSET` already included `/rest`.
7
+ */
8
+ function stripDuplicateMountPrefix(pathOrUrl, baseURL) {
9
+ const qIdx = pathOrUrl.indexOf('?');
10
+ const pathOnly = qIdx >= 0 ? pathOrUrl.slice(0, qIdx) : pathOrUrl;
11
+ const query = qIdx >= 0 ? pathOrUrl.slice(qIdx) : '';
12
+ const mount = mountPathFromBaseURL(baseURL);
13
+ if (!mount) {
14
+ return pathOrUrl;
15
+ }
16
+ let p = pathOnly;
17
+ while (p.startsWith(`${mount}/`) || p === mount) {
18
+ if (p === mount) {
19
+ p = '/';
20
+ break;
21
+ }
22
+ p = p.slice(mount.length);
23
+ if (!p.startsWith('/')) {
24
+ p = `/${p}`;
25
+ }
26
+ }
27
+ return p + query;
28
+ }
29
+ /** Turn `__ENTITYSET` / `Content-Location` into a path or absolute URL `QuadroHttp.request` accepts. */
30
+ function parseEntitySetUri(raw, baseURL) {
31
+ const base = normalizeBaseURL(baseURL);
32
+ if (raw.startsWith('http://') || raw.startsWith('https://')) {
33
+ if (raw.startsWith(base)) {
34
+ const rest = raw.slice(base.length);
35
+ const path = rest.startsWith('/') ? rest : `/${rest}`;
36
+ return stripDuplicateMountPrefix(path, baseURL);
37
+ }
38
+ return raw;
39
+ }
40
+ let path = raw.startsWith('/') ? raw : `/${raw}`;
41
+ const basePath = mountPathFromBaseURL(baseURL);
42
+ if (basePath && path.startsWith(`${basePath}/`)) {
43
+ path = path.slice(basePath.length);
44
+ if (!path.startsWith('/')) {
45
+ path = `/${path}`;
46
+ }
47
+ }
48
+ return stripDuplicateMountPrefix(path, baseURL);
49
+ }
50
+ function appendReleaseQuery(pathOrUrl) {
51
+ const sep = pathOrUrl.includes('?') ? '&' : '?';
52
+ return `${pathOrUrl}${sep}$method=release`;
53
+ }
3
54
  function primaryKeyFromRow(row, keyNames) {
4
55
  if (!row || typeof row !== 'object') {
5
56
  return undefined;
@@ -17,22 +68,80 @@ function primaryKeyFromRow(row, keyNames) {
17
68
  */
18
69
  export function createCollection(ctx, initialOptions, mapRow) {
19
70
  const seenIds = new Set();
20
- let entitySetUrl = ctx.entitySetUrl;
71
+ let entitySetUrl = ctx.entitySetUrl ??
72
+ (initialOptions.reuseEntitySet
73
+ ? parseEntitySetUri(initialOptions.reuseEntitySet, ctx.http.baseURL)
74
+ : undefined);
75
+ const useEntitySet = initialOptions.entitySet !== false;
21
76
  async function fetchPage(page) {
22
- const qs = buildListSearchParams(ctx.className, { ...initialOptions, page }, ctx.relationMap);
23
- const res = await ctx.http.request(`${ctx.path}${qs}`, {
77
+ const pageSize = initialOptions.pageSize ?? 50;
78
+ const skip = (page - 1) * pageSize;
79
+ async function parseEntitySetFromResponse(json, res) {
80
+ let fromBody;
81
+ if (json && typeof json === 'object' && '__ENTITYSET' in json) {
82
+ const u = json.__ENTITYSET;
83
+ if (typeof u === 'string') {
84
+ fromBody = u;
85
+ }
86
+ }
87
+ const loc = res.headers.get('Content-Location') ?? res.headers.get('Location');
88
+ const raw = fromBody ?? loc ?? undefined;
89
+ if (raw) {
90
+ entitySetUrl = parseEntitySetUri(raw, ctx.http.baseURL);
91
+ if (!initialOptions.reuseEntitySet) {
92
+ initialOptions.onEntitySetReady?.(entitySetUrl);
93
+ }
94
+ }
95
+ }
96
+ if (useEntitySet && !entitySetUrl) {
97
+ const qsCreate = buildEntitySetCreationParams(initialOptions);
98
+ const createPath = `${ctx.path}${qsCreate}`;
99
+ const resCreate = await ctx.http.request(createPath, {
100
+ signal: initialOptions.signal,
101
+ });
102
+ const textCreate = await resCreate.text();
103
+ if (!resCreate.ok) {
104
+ throw new Error(`Entity set creation failed ${resCreate.status}: ${textCreate.slice(0, 200)}`);
105
+ }
106
+ const jsonCreate = textCreate ? JSON.parse(textCreate) : [];
107
+ await parseEntitySetFromResponse(jsonCreate, resCreate);
108
+ if (!entitySetUrl) {
109
+ return unwrapEntityList(ctx.className, jsonCreate);
110
+ }
111
+ const qsPage = buildEntitySetPageParams(skip, pageSize, initialOptions.select);
112
+ const base = entitySetUrl;
113
+ const pagePath = qsPage
114
+ ? base.includes('?')
115
+ ? `${base}&${qsPage.slice(1)}`
116
+ : `${base}${qsPage}`
117
+ : base;
118
+ const res = await ctx.http.request(pagePath, {
119
+ signal: initialOptions.signal,
120
+ });
121
+ const text = await res.text();
122
+ if (!res.ok) {
123
+ throw new Error(`List failed ${res.status}: ${text.slice(0, 200)}`);
124
+ }
125
+ const json = text ? JSON.parse(text) : [];
126
+ return unwrapEntityList(ctx.className, json);
127
+ }
128
+ let requestPath;
129
+ if (entitySetUrl && useEntitySet) {
130
+ const qs = buildEntitySetPageParams(skip, pageSize, initialOptions.select);
131
+ const base = entitySetUrl;
132
+ requestPath = qs ? (base.includes('?') ? `${base}&${qs.slice(1)}` : `${base}${qs}`) : base;
133
+ }
134
+ else {
135
+ const qs = buildListSearchParams(ctx.className, { ...initialOptions, page }, ctx.relationMap);
136
+ requestPath = `${ctx.path}${qs}`;
137
+ }
138
+ const res = await ctx.http.request(requestPath, {
24
139
  signal: initialOptions.signal,
25
140
  });
26
141
  const text = await res.text();
27
142
  if (!res.ok) {
28
143
  throw new Error(`List failed ${res.status}: ${text.slice(0, 200)}`);
29
144
  }
30
- const loc = res.headers.get('Content-Location') ?? res.headers.get('Location');
31
- if (loc) {
32
- entitySetUrl = loc.startsWith('http')
33
- ? loc
34
- : `${ctx.http.baseURL}${loc.startsWith('/') ? '' : '/'}${loc}`;
35
- }
36
145
  const json = text ? JSON.parse(text) : [];
37
146
  return unwrapEntityList(ctx.className, json);
38
147
  }
@@ -85,16 +194,18 @@ export function createCollection(ctx, initialOptions, mapRow) {
85
194
  seenIds.clear();
86
195
  },
87
196
  async release() {
88
- if (entitySetUrl) {
89
- try {
90
- await ctx.http.void(entitySetUrl.replace(ctx.http.baseURL, '') || entitySetUrl, {
91
- method: 'DELETE',
92
- });
93
- }
94
- catch {
95
- // Some servers ignore release; swallow
96
- }
97
- entitySetUrl = undefined;
197
+ if (!entitySetUrl) {
198
+ return;
199
+ }
200
+ const toRelease = entitySetUrl;
201
+ entitySetUrl = undefined;
202
+ try {
203
+ const path = appendReleaseQuery(toRelease);
204
+ const res = await ctx.http.request(path, { method: 'GET' });
205
+ await res.text();
206
+ }
207
+ catch {
208
+ // 4D may return an error if already expired; ignore
98
209
  }
99
210
  },
100
211
  get length() {
@@ -28,15 +28,22 @@ export interface EntityNavigationConfig {
28
28
  relationMap: Record<string, string>;
29
29
  keyNames: Record<string, readonly string[]>;
30
30
  }
31
+ export interface RelatedNavigationSpec {
32
+ attr: string;
33
+ targetClass: string;
34
+ /** Entity-selection class methods for `targetClass` (4D `entityCollection`). */
35
+ entityCollectionMethodNames?: readonly string[];
36
+ }
37
+ /** Bind entity-selection class methods onto a collection handle or `{ list }` API object. */
38
+ export declare function bindEntityCollectionMethods<H extends object>(target: H, cfg: Pick<DataClassRuntimeConfig, 'http' | 'className' | 'relationMap'>, methodNames: readonly string[]): H;
39
+ /** Attach entity class methods (4D `entity`) on a mapped row. */
40
+ export declare function attachEntityClassMethods(row: unknown, cfg: Pick<DataClassRuntimeConfig, 'http' | 'className' | 'relationMap' | 'keyNames'>, methodNames: readonly string[]): void;
31
41
  /** Nested collection under an entity, e.g. `Agency(1)/todayBookings`. */
32
- export declare function makeRelatedCollectionApi(cfg: EntityNavigationConfig, attributeName: string, targetClassName: string): {
42
+ export declare function makeRelatedCollectionApi(cfg: EntityNavigationConfig, attributeName: string, targetClassName: string, entityCollectionMethodNames?: readonly string[]): {
33
43
  list<S extends readonly string[] = readonly []>(options?: CollectionOptions & {
34
44
  select?: S;
35
45
  }): CollectionHandle<unknown>;
36
46
  };
37
- export declare function attachRelatedApis(row: unknown, cfg: EntityNavigationConfig, relations: readonly {
38
- attr: string;
39
- targetClass: string;
40
- }[]): void;
47
+ export declare function attachRelatedApis(row: unknown, cfg: EntityNavigationConfig, relations: readonly RelatedNavigationSpec[]): void;
41
48
  export { unwrapEntityList };
42
49
  //# sourceMappingURL=data-class.d.ts.map
@@ -1 +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,wGAAwG;IACxG,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"}
1
+ {"version":3,"file":"data-class.d.ts","sourceRoot":"","sources":["../../src/runtime/data-class.ts"],"names":[],"mappings":"AAMA,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,wGAAwG;IACxG,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,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,gFAAgF;IAChF,2BAA2B,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAChD;AAED,6FAA6F;AAC7F,wBAAgB,2BAA2B,CAAC,CAAC,SAAS,MAAM,EAC1D,MAAM,EAAE,CAAC,EACT,GAAG,EAAE,IAAI,CAAC,sBAAsB,EAAE,MAAM,GAAG,WAAW,GAAG,aAAa,CAAC,EACvE,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,CAAC,CAiBH;AAgBD,iEAAiE;AACjE,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,IAAI,CAAC,sBAAsB,EAAE,MAAM,GAAG,WAAW,GAAG,aAAa,GAAG,UAAU,CAAC,EACpF,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,IAAI,CAmBN;AAED,yEAAyE;AACzE,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,sBAAsB,EAC3B,aAAa,EAAE,MAAM,EACrB,eAAe,EAAE,MAAM,EACvB,2BAA2B,CAAC,EAAE,SAAS,MAAM,EAAE;SAKxC,CAAC,SAAS,SAAS,MAAM,EAAE,0BACpB,iBAAiB,GAAG;QAAE,MAAM,CAAC,EAAE,CAAC,CAAA;KAAE,GAC3C,gBAAgB,CAAC,OAAO,CAAC;EAmB/B;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,SAAS,qBAAqB,EAAE,GAC1C,IAAI,CAYN;AAED,OAAO,EAAE,gBAAgB,EAAE,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { callEntityCollectionFunction, callEntityFunction, } from './class-function.mjs';
1
2
  import { createCollection } from './collection.mjs';
2
3
  import { buildEntityParams } from './query.mjs';
3
4
  import { unwrapEntity, unwrapEntityList } from './unwrap.mjs';
@@ -68,10 +69,52 @@ function formatFilterLiteral(p) {
68
69
  const s = String(p).replace(/'/g, "''");
69
70
  return `'${s}'`;
70
71
  }
72
+ /** Bind entity-selection class methods onto a collection handle or `{ list }` API object. */
73
+ export function bindEntityCollectionMethods(target, cfg, methodNames) {
74
+ if (!methodNames.length) {
75
+ return target;
76
+ }
77
+ const o = target;
78
+ for (const name of methodNames) {
79
+ o[name] = (args = [], init) => callEntityCollectionFunction(cfg.http, cfg.className, cfg.relationMap, name, args, init);
80
+ }
81
+ return target;
82
+ }
83
+ function primaryKeyFromRow(row, keyNames) {
84
+ if (!row || typeof row !== 'object') {
85
+ return undefined;
86
+ }
87
+ const o = row;
88
+ for (const k of keyNames) {
89
+ const v = o[k];
90
+ if (v !== undefined && v !== null) {
91
+ return v;
92
+ }
93
+ }
94
+ return undefined;
95
+ }
96
+ /** Attach entity class methods (4D `entity`) on a mapped row. */
97
+ export function attachEntityClassMethods(row, cfg, methodNames) {
98
+ if (!row || typeof row !== 'object' || !methodNames.length) {
99
+ return;
100
+ }
101
+ const pk = primaryKeyFromRow(row, cfg.keyNames);
102
+ if (pk === undefined) {
103
+ return;
104
+ }
105
+ const obj = row;
106
+ for (const name of methodNames) {
107
+ Object.defineProperty(obj, name, {
108
+ enumerable: false,
109
+ configurable: true,
110
+ value: (args = [], init) => callEntityFunction(cfg.http, cfg.className, pk, name, args, init),
111
+ });
112
+ }
113
+ }
71
114
  /** Nested collection under an entity, e.g. `Agency(1)/todayBookings`. */
72
- export function makeRelatedCollectionApi(cfg, attributeName, targetClassName) {
115
+ export function makeRelatedCollectionApi(cfg, attributeName, targetClassName, entityCollectionMethodNames) {
73
116
  const basePath = `/${cfg.parentClass}(${encodeURIComponent(String(cfg.parentId))})/${attributeName}`;
74
- return {
117
+ const api = {
75
118
  list(options) {
76
119
  return createCollection({
77
120
  http: cfg.http,
@@ -82,17 +125,18 @@ export function makeRelatedCollectionApi(cfg, attributeName, targetClassName) {
82
125
  }, options ?? {}, (raw) => raw);
83
126
  },
84
127
  };
128
+ return bindEntityCollectionMethods(api, { http: cfg.http, className: targetClassName, relationMap: cfg.relationMap }, entityCollectionMethodNames ?? []);
85
129
  }
86
130
  export function attachRelatedApis(row, cfg, relations) {
87
131
  if (!row || typeof row !== 'object') {
88
132
  return;
89
133
  }
90
134
  const obj = row;
91
- for (const { attr, targetClass } of relations) {
135
+ for (const { attr, targetClass, entityCollectionMethodNames } of relations) {
92
136
  Object.defineProperty(obj, attr, {
93
137
  enumerable: false,
94
138
  configurable: true,
95
- value: makeRelatedCollectionApi(cfg, attr, targetClass),
139
+ value: makeRelatedCollectionApi(cfg, attr, targetClass, entityCollectionMethodNames),
96
140
  });
97
141
  }
98
142
  }
@@ -5,6 +5,11 @@ export interface QuadroFetchOptions {
5
5
  defaultHeaders?: Record<string, string>;
6
6
  }
7
7
  export declare function normalizeBaseURL(baseURL: string): string;
8
+ /**
9
+ * REST mount pathname (e.g. `/rest`) for joining paths and stripping duplicates.
10
+ * Supports relative bases like `/rest` — `new URL('/rest')` is invalid, so callers must not use that alone.
11
+ */
12
+ export declare function mountPathFromBaseURL(baseURL: string): string;
8
13
  export declare class QuadroHttp {
9
14
  private readonly opts;
10
15
  constructor(opts: QuadroFetchOptions);
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/runtime/http.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,wEAAwE;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACxC;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,kBAAkB;IAErD,IAAI,OAAO,IAAI,MAAM,CAEpB;IAEK,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAwBhE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,CAAC,CAAC;IAYzD,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;CAOhE"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/runtime/http.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,wEAAwE;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACxC;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAa5D;AAED,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,kBAAkB;IAErD,IAAI,OAAO,IAAI,MAAM,CAEpB;IAEK,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAwBhE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,CAAC,CAAC;IAYzD,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;CAOhE"}
@@ -2,6 +2,25 @@ import { QuadroHttpError } from './errors.mjs';
2
2
  export function normalizeBaseURL(baseURL) {
3
3
  return baseURL.replace(/\/$/, '');
4
4
  }
5
+ /**
6
+ * REST mount pathname (e.g. `/rest`) for joining paths and stripping duplicates.
7
+ * Supports relative bases like `/rest` — `new URL('/rest')` is invalid, so callers must not use that alone.
8
+ */
9
+ export function mountPathFromBaseURL(baseURL) {
10
+ const b = normalizeBaseURL(baseURL);
11
+ if (b.startsWith('http://') || b.startsWith('https://')) {
12
+ try {
13
+ return new URL(b).pathname.replace(/\/$/, '') || '';
14
+ }
15
+ catch {
16
+ return '';
17
+ }
18
+ }
19
+ if (b.startsWith('/')) {
20
+ return b.replace(/\/$/, '') || '';
21
+ }
22
+ return '';
23
+ }
5
24
  export class QuadroHttp {
6
25
  opts;
7
26
  constructor(opts) {
@@ -1,9 +1,10 @@
1
+ export { type ClassFunctionHttpOptions, callDataClassFunction, callEntityCollectionFunction, callEntityFunction, type EntityCollectionMethodOptions, unwrapClassFunctionResult, } from './class-function.js';
1
2
  export { type CollectionContext, type CollectionHandle, type CollectionOptions, createCollection, } from './collection.js';
2
- export { attachRelatedApis, type DataClassRuntimeConfig, type EntityNavigationConfig, makeDataClassApi, makeRelatedCollectionApi, } from './data-class.js';
3
+ export { attachEntityClassMethods, attachRelatedApis, bindEntityCollectionMethods, type DataClassRuntimeConfig, type EntityNavigationConfig, makeDataClassApi, makeRelatedCollectionApi, type RelatedNavigationSpec, } from './data-class.js';
3
4
  export { callDatastorePath, createDatastoreNamespace } from './datastore.js';
4
5
  export { QuadroHttpError } from './errors.js';
5
- export { normalizeBaseURL, type QuadroFetchOptions, QuadroHttp, } from './http.js';
6
+ export { mountPathFromBaseURL, normalizeBaseURL, type QuadroFetchOptions, QuadroHttp, } from './http.js';
6
7
  export type { Paths1, SelectedEntity } from './paths.js';
7
- export { buildListSearchParams, type ListQueryParams } from './query.js';
8
+ export { buildEntitySetCreationParams, buildEntitySetPageParams, buildListSearchParams, buildMethodSelectionQuery, type ListQueryParams, type MethodSelectionQuery, } from './query.js';
8
9
  export { unwrapEntity, unwrapEntityList } from './unwrap.js';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,gBAAgB,GACjB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,iBAAiB,EACjB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAA;AAC5E,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EACL,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,UAAU,GACX,MAAM,WAAW,CAAA;AAClB,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,EAAE,qBAAqB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAA;AACxE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,wBAAwB,EAC7B,qBAAqB,EACrB,4BAA4B,EAC5B,kBAAkB,EAClB,KAAK,6BAA6B,EAClC,yBAAyB,GAC1B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,gBAAgB,GACjB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,2BAA2B,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,wBAAwB,EACxB,KAAK,qBAAqB,GAC3B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAA;AAC5E,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,UAAU,GACX,MAAM,WAAW,CAAA;AAClB,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,EACL,4BAA4B,EAC5B,wBAAwB,EACxB,qBAAqB,EACrB,yBAAyB,EACzB,KAAK,eAAe,EACpB,KAAK,oBAAoB,GAC1B,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA"}
@@ -1,7 +1,8 @@
1
+ export { callDataClassFunction, callEntityCollectionFunction, callEntityFunction, unwrapClassFunctionResult, } from './class-function.mjs';
1
2
  export { createCollection, } from './collection.mjs';
2
- export { attachRelatedApis, makeDataClassApi, makeRelatedCollectionApi, } from './data-class.mjs';
3
+ export { attachEntityClassMethods, attachRelatedApis, bindEntityCollectionMethods, makeDataClassApi, makeRelatedCollectionApi, } from './data-class.mjs';
3
4
  export { callDatastorePath, createDatastoreNamespace } from './datastore.mjs';
4
5
  export { QuadroHttpError } from './errors.mjs';
5
- export { normalizeBaseURL, QuadroHttp, } from './http.mjs';
6
- export { buildListSearchParams } from './query.mjs';
6
+ export { mountPathFromBaseURL, normalizeBaseURL, QuadroHttp, } from './http.mjs';
7
+ export { buildEntitySetCreationParams, buildEntitySetPageParams, buildListSearchParams, buildMethodSelectionQuery, } from './query.mjs';
7
8
  export { unwrapEntity, unwrapEntityList } from './unwrap.mjs';
@@ -10,8 +10,40 @@ export interface ListQueryParams {
10
10
  /** OData-style filter string (4D REST). */
11
11
  filter?: string;
12
12
  orderby?: string;
13
+ /**
14
+ * When true (default for list collections), append `$method=entityset` so 4D creates a cached
15
+ * entity set ([docs](https://developer.4d.com/docs/REST/method#methodentityset)). Call
16
+ * `release()` on the collection handle with `$method=release` when finished
17
+ * ([docs](https://developer.4d.com/docs/REST/method#methodrelease)). Set `false` if the server
18
+ * does not support entity sets.
19
+ */
20
+ entitySet?: boolean;
13
21
  }
14
22
  export declare function buildListSearchParams(_className: string, opts: ListQueryParams, _relationMap: Record<string, string>): string;
23
+ /**
24
+ * First request to create an entity set: [`$method=entityset`](https://developer.4d.com/docs/REST/method#methodentityset)
25
+ * with `$top=0` so the set is created without returning a full page; follow with
26
+ * {@link buildEntitySetPageParams} on `__ENTITYSET` for real paging.
27
+ */
28
+ export declare function buildEntitySetCreationParams(opts: ListQueryParams): string;
29
+ /**
30
+ * Query string for paging an existing entity set (`$skip`, `$top`, `$attributes`).
31
+ * Used after the first [`$method=entityset`](https://developer.4d.com/docs/REST/method#methodentityset) response.
32
+ */
33
+ export declare function buildEntitySetPageParams(skip: number, pageSize: number, select?: readonly string[]): string;
15
34
  /** Query string for a single entity (`$attributes` / no paging). */
16
35
  export declare function buildEntityParams(_className: string, select: readonly string[] | undefined, _relationMap: Record<string, string>): string;
36
+ /** Optional list constraints for entity-selection class functions (not the same as `$method=entityset` cache). */
37
+ export type MethodSelectionQuery = {
38
+ page?: number;
39
+ pageSize?: number;
40
+ select?: readonly string[];
41
+ filter?: string;
42
+ orderby?: string;
43
+ };
44
+ /**
45
+ * Query string for entity-selection class functions: `$filter`, `$orderby`, `$attributes`,
46
+ * `$skip`, `$top` ([4D class functions](https://developer.4d.com/docs/REST/classFunctions.html)).
47
+ */
48
+ export declare function buildMethodSelectionQuery(_className: string, _relationMap: Record<string, string>, selection?: MethodSelectionQuery): string;
17
49
  //# sourceMappingURL=query.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/runtime/query.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1B,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AASD,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,eAAe,EACrB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACnC,MAAM,CAqBR;AAED,oEAAoE;AACpE,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,EACrC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACnC,MAAM,CAQR"}
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/runtime/query.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1B,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AASD,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,eAAe,EACrB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACnC,MAAM,CAwBR;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAgB1E;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,GACzB,MAAM,CAYR;AAED,oEAAoE;AACpE,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,EACrC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACnC,MAAM,CAQR;AAED,kHAAkH;AAClH,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,SAAS,CAAC,EAAE,oBAAoB,GAC/B,MAAM,CAyBR"}
@@ -24,6 +24,48 @@ export function buildListSearchParams(_className, opts, _relationMap) {
24
24
  if (attrs) {
25
25
  params.set('$attributes', attrs);
26
26
  }
27
+ if (opts.entitySet) {
28
+ params.set('$method', 'entityset');
29
+ }
30
+ const q = params.toString();
31
+ return q ? `?${q}` : '';
32
+ }
33
+ /**
34
+ * First request to create an entity set: [`$method=entityset`](https://developer.4d.com/docs/REST/method#methodentityset)
35
+ * with `$top=0` so the set is created without returning a full page; follow with
36
+ * {@link buildEntitySetPageParams} on `__ENTITYSET` for real paging.
37
+ */
38
+ export function buildEntitySetCreationParams(opts) {
39
+ const params = new URLSearchParams();
40
+ params.set('$top', '0');
41
+ if (opts.filter) {
42
+ params.set('$filter', opts.filter);
43
+ }
44
+ if (opts.orderby) {
45
+ params.set('$orderby', opts.orderby);
46
+ }
47
+ const attrs = attributesParam(opts.select ?? []);
48
+ if (attrs) {
49
+ params.set('$attributes', attrs);
50
+ }
51
+ params.set('$method', 'entityset');
52
+ const q = params.toString();
53
+ return q ? `?${q}` : '';
54
+ }
55
+ /**
56
+ * Query string for paging an existing entity set (`$skip`, `$top`, `$attributes`).
57
+ * Used after the first [`$method=entityset`](https://developer.4d.com/docs/REST/method#methodentityset) response.
58
+ */
59
+ export function buildEntitySetPageParams(skip, pageSize, select) {
60
+ const params = new URLSearchParams();
61
+ if (skip > 0) {
62
+ params.set('$skip', String(skip));
63
+ }
64
+ params.set('$top', String(pageSize));
65
+ const attrs = attributesParam(select ?? []);
66
+ if (attrs) {
67
+ params.set('$attributes', attrs);
68
+ }
27
69
  const q = params.toString();
28
70
  return q ? `?${q}` : '';
29
71
  }
@@ -37,3 +79,34 @@ export function buildEntityParams(_className, select, _relationMap) {
37
79
  const q = params.toString();
38
80
  return q ? `?${q}` : '';
39
81
  }
82
+ /**
83
+ * Query string for entity-selection class functions: `$filter`, `$orderby`, `$attributes`,
84
+ * `$skip`, `$top` ([4D class functions](https://developer.4d.com/docs/REST/classFunctions.html)).
85
+ */
86
+ export function buildMethodSelectionQuery(_className, _relationMap, selection) {
87
+ if (!selection) {
88
+ return '';
89
+ }
90
+ const params = new URLSearchParams();
91
+ if (selection.page != null && selection.pageSize != null) {
92
+ const skip = (selection.page - 1) * selection.pageSize;
93
+ if (skip > 0) {
94
+ params.set('$skip', String(skip));
95
+ }
96
+ params.set('$top', String(selection.pageSize));
97
+ }
98
+ else if (selection.pageSize != null) {
99
+ params.set('$top', String(selection.pageSize));
100
+ }
101
+ if (selection.filter) {
102
+ params.set('$filter', selection.filter);
103
+ }
104
+ if (selection.orderby) {
105
+ params.set('$orderby', selection.orderby);
106
+ }
107
+ if (selection.select?.length) {
108
+ params.set('$attributes', selection.select.join(','));
109
+ }
110
+ const q = params.toString();
111
+ return q ? `?${q}` : '';
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quadrokit/client",
3
- "version": "0.2.13",
3
+ "version": "0.3.0",
4
4
  "description": "Typed 4D REST client and catalog code generator for QuadroKit",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -29,7 +29,7 @@
29
29
  "generate:fixture": "bun run src/cli.ts generate --url file://../../assets/catalog.json --out ../../.quadrokit/generated-demo"
30
30
  },
31
31
  "dependencies": {
32
- "@quadrokit/shared": "^0.2.13",
32
+ "@quadrokit/shared": "^0.3.0",
33
33
  "undici": "^6.21.0"
34
34
  },
35
35
  "peerDependencies": {