@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 +189 -17
- package/dist/generate/codegen.d.ts.map +1 -1
- package/dist/generate/codegen.mjs +38 -4
- package/dist/runtime/class-function.d.ts +44 -0
- package/dist/runtime/class-function.d.ts.map +1 -0
- package/dist/runtime/class-function.mjs +93 -0
- package/dist/runtime/collection.d.ts +8 -1
- package/dist/runtime/collection.d.ts.map +1 -1
- package/dist/runtime/collection.mjs +131 -20
- package/dist/runtime/data-class.d.ts +12 -5
- package/dist/runtime/data-class.d.ts.map +1 -1
- package/dist/runtime/data-class.mjs +48 -4
- package/dist/runtime/http.d.ts +5 -0
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.mjs +19 -0
- package/dist/runtime/index.d.ts +4 -3
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.mjs +4 -3
- package/dist/runtime/query.d.ts +32 -0
- package/dist/runtime/query.d.ts.map +1 -1
- package/dist/runtime/query.mjs +73 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,47 +1,219 @@
|
|
|
1
1
|
# `@quadrokit/client`
|
|
2
2
|
|
|
3
|
-
Typed **4D REST**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
|
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;
|
|
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
|
-
?
|
|
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(
|
|
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
|
|
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,
|
|
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 {
|
|
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
|
|
23
|
-
const
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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":"
|
|
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
|
-
|
|
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
|
}
|
package/dist/runtime/http.d.ts
CHANGED
|
@@ -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"}
|
package/dist/runtime/http.mjs
CHANGED
|
@@ -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) {
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/runtime/index.mjs
CHANGED
|
@@ -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';
|
package/dist/runtime/query.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/runtime/query.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
32
|
+
"@quadrokit/shared": "^0.3.0",
|
|
33
33
|
"undici": "^6.21.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|