@quadrokit/client 0.1.0 → 0.2.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.
Files changed (49) hide show
  1. package/dist/cli.d.ts +3 -0
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +69 -0
  4. package/dist/generate/codegen.d.ts +3 -0
  5. package/dist/generate/codegen.d.ts.map +1 -0
  6. package/dist/generate/codegen.js +282 -0
  7. package/dist/index.d.ts +6 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/{src/index.ts → dist/index.js} +1 -1
  10. package/dist/runtime/collection.d.ts +29 -0
  11. package/dist/runtime/collection.d.ts.map +1 -0
  12. package/dist/runtime/collection.js +94 -0
  13. package/dist/runtime/data-class.d.ts +42 -0
  14. package/dist/runtime/data-class.d.ts.map +1 -0
  15. package/dist/runtime/data-class.js +99 -0
  16. package/dist/runtime/datastore.d.ts +8 -0
  17. package/dist/runtime/datastore.d.ts.map +1 -0
  18. package/dist/runtime/datastore.js +15 -0
  19. package/dist/runtime/errors.d.ts +6 -0
  20. package/dist/runtime/errors.d.ts.map +1 -0
  21. package/dist/runtime/errors.js +10 -0
  22. package/dist/runtime/http.d.ts +16 -0
  23. package/dist/runtime/http.d.ts.map +1 -0
  24. package/dist/runtime/http.js +54 -0
  25. package/dist/runtime/index.d.ts +9 -0
  26. package/dist/runtime/index.d.ts.map +1 -0
  27. package/dist/runtime/index.js +7 -0
  28. package/dist/runtime/paths.d.ts +16 -0
  29. package/dist/runtime/paths.d.ts.map +1 -0
  30. package/dist/runtime/paths.js +2 -0
  31. package/dist/runtime/query.d.ts +14 -0
  32. package/dist/runtime/query.d.ts.map +1 -0
  33. package/dist/runtime/query.js +76 -0
  34. package/dist/runtime/unwrap.d.ts +4 -0
  35. package/dist/runtime/unwrap.d.ts.map +1 -0
  36. package/dist/runtime/unwrap.js +31 -0
  37. package/package.json +13 -6
  38. package/src/cli.ts +0 -82
  39. package/src/generate/codegen.ts +0 -317
  40. package/src/runtime/collection.ts +0 -135
  41. package/src/runtime/data-class.ts +0 -156
  42. package/src/runtime/datastore.ts +0 -25
  43. package/src/runtime/errors.ts +0 -11
  44. package/src/runtime/http.ts +0 -64
  45. package/src/runtime/index.ts +0 -23
  46. package/src/runtime/paths.ts +0 -42
  47. package/src/runtime/query.ts +0 -104
  48. package/src/runtime/unwrap.ts +0 -33
  49. package/tsconfig.json +0 -9
@@ -1,317 +0,0 @@
1
- import { mkdir, writeFile } from 'node:fs/promises'
2
- import path from 'node:path'
3
- import type { CatalogAttribute, CatalogDataClass, CatalogJson } from '@quadrokit/shared'
4
- import { sessionCookieName } from '@quadrokit/shared'
5
-
6
- function map4dType(attr: CatalogAttribute): string {
7
- const t = attr.type ?? 'unknown'
8
- switch (t) {
9
- case 'long':
10
- case 'word':
11
- return 'number'
12
- case 'string':
13
- return 'string'
14
- case 'bool':
15
- return 'boolean'
16
- case 'number':
17
- return 'number'
18
- case 'date':
19
- return 'string'
20
- case 'duration':
21
- return 'string | number'
22
- case 'image':
23
- return 'string | null'
24
- case 'object':
25
- return 'Record<string, unknown>'
26
- default: {
27
- if (attr.kind === 'relatedEntity') {
28
- return `${t} | null`
29
- }
30
- if (attr.kind === 'relatedEntities' || attr.kind === 'calculated') {
31
- return 'unknown'
32
- }
33
- return 'unknown'
34
- }
35
- }
36
- }
37
-
38
- function selectionToEntity(selectionType: string): string {
39
- if (selectionType.endsWith('Selection')) {
40
- return selectionType.slice(0, -'Selection'.length)
41
- }
42
- return selectionType
43
- }
44
-
45
- function navigableRelations(dc: CatalogDataClass): { attr: string; targetClass: string }[] {
46
- const out: { attr: string; targetClass: string }[] = []
47
- for (const a of dc.attributes ?? []) {
48
- if (a.kind === 'relatedEntities') {
49
- out.push({
50
- attr: a.name,
51
- targetClass: selectionToEntity(a.type ?? 'unknown'),
52
- })
53
- }
54
- if (a.kind === 'calculated' && a.behavior === 'relatedEntities' && a.type) {
55
- out.push({ attr: a.name, targetClass: selectionToEntity(a.type) })
56
- }
57
- }
58
- return out
59
- }
60
-
61
- function relationTargets(dc: CatalogDataClass): Record<string, string> {
62
- const m: Record<string, string> = {}
63
- for (const a of dc.attributes ?? []) {
64
- if (a.kind === 'relatedEntity' && a.type) {
65
- m[a.name] = a.type
66
- }
67
- }
68
- return m
69
- }
70
-
71
- function keyNames(dc: CatalogDataClass): string[] {
72
- const k = dc.key?.map((x) => x.name) ?? ['ID']
73
- return k.length ? k : ['ID']
74
- }
75
-
76
- function collectPaths(
77
- dc: CatalogDataClass,
78
- byName: Map<string, CatalogDataClass>,
79
- maxDepth: number
80
- ): string[] {
81
- const paths = new Set<string>()
82
- const visit = (current: CatalogDataClass, prefix: string, depth: number) => {
83
- for (const a of current.attributes ?? []) {
84
- if (a.kind === 'storage' || a.kind === 'calculated' || a.kind === 'alias') {
85
- const p = prefix ? `${prefix}.${a.name}` : a.name
86
- paths.add(p)
87
- }
88
- if (a.kind === 'relatedEntity' && a.type && depth < maxDepth) {
89
- const child = byName.get(a.type)
90
- const base = prefix ? `${prefix}.${a.name}` : a.name
91
- paths.add(base)
92
- if (child) {
93
- visit(child, base, depth + 1)
94
- }
95
- }
96
- }
97
- }
98
- visit(dc, '', 0)
99
- return [...paths].sort()
100
- }
101
-
102
- function emitInterface(dc: CatalogDataClass, _byName: Map<string, CatalogDataClass>): string {
103
- const lines: string[] = []
104
- lines.push(`export interface ${dc.name} {`)
105
- for (const a of dc.attributes ?? []) {
106
- if (a.kind === 'relatedEntities') {
107
- continue
108
- }
109
- if (a.kind === 'calculated' && a.behavior === 'relatedEntities') {
110
- continue
111
- }
112
- const opt = a.not_null || a.identifying ? '' : '?'
113
- const ts = map4dType(a)
114
- lines.push(` ${a.name}${opt}: ${ts};`)
115
- }
116
- lines.push('}')
117
- return lines.join('\n')
118
- }
119
-
120
- function emitTypes(catalog: CatalogJson): string {
121
- const classes = catalog.dataClasses?.filter((d) => d.exposed) ?? []
122
- const byName = new Map(classes.map((c) => [c.name, c]))
123
- const interfaces = classes.map((c) => emitInterface(c, byName))
124
- const pathTypes = classes.map((c) => {
125
- const paths = collectPaths(c, byName, 2)
126
- const name = `${c.name}Path`
127
- const body = paths.length ? paths.map((p) => `'${p}'`).join('\n | ') : 'never'
128
- return `export type ${name} =\n | ${body};`
129
- })
130
- return `/* eslint-disable */\n/* Auto-generated by @quadrokit/client — do not edit */\n\n${interfaces.join('\n\n')}\n\n${pathTypes.join('\n\n')}\n`
131
- }
132
-
133
- function emitClient(catalog: CatalogJson): string {
134
- const classes = catalog.dataClasses?.filter((d) => d.exposed) ?? []
135
- const dbName = catalog.__NAME ?? 'default'
136
- const hasAuthentify = catalog.methods?.some(
137
- (m) => m.name === 'authentify' && m.applyTo === 'dataStore'
138
- )
139
-
140
- const keyNamesRecord = Object.fromEntries(classes.map((x) => [x.name, keyNames(x)] as const))
141
-
142
- const imports = `/* 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`
143
-
144
- const typeImports = classes.map((c) => c.name).join(', ')
145
- const pathImports = classes.map((c) => `${c.name}Path`).join(', ')
146
- const header = `${imports}\nimport type { ${typeImports}, ${pathImports} } from './types.gen.js';\n\n`
147
-
148
- const metaExport = `
149
- export const quadrokitCatalogMeta = {
150
- __NAME: ${JSON.stringify(dbName)},
151
- sessionCookieName: ${JSON.stringify(sessionCookieName(catalog))},
152
- } as const;
153
- `
154
-
155
- const configInterface = `
156
- export interface QuadroClientConfig {
157
- baseURL: string;
158
- fetchImpl?: typeof fetch;
159
- defaultHeaders?: Record<string, string>;
160
- }
161
- `
162
-
163
- const mapFunctions = classes
164
- .map((c) => {
165
- const nav = navigableRelations(c)
166
- const relMap = JSON.stringify(relationTargets(c))
167
- return ` function map${c.name}Row(http: QuadroHttp, raw: unknown): ${c.name} {
168
- const row = raw as ${c.name};
169
- const pk = (row as { ID?: number }).ID ?? (row as { id?: number }).id;
170
- attachRelatedApis(
171
- raw,
172
- {
173
- http,
174
- parentClass: '${c.name}',
175
- parentId: pk as number,
176
- relationMap: ${relMap},
177
- keyNames: ${JSON.stringify(keyNamesRecord)},
178
- },
179
- ${JSON.stringify(nav)},
180
- );
181
- return row;
182
- }`
183
- })
184
- .join('\n\n')
185
-
186
- const classBranches = classes
187
- .map((c) => {
188
- const relMap = JSON.stringify(relationTargets(c))
189
- const kn = JSON.stringify(keyNames(c))
190
- const pathsType = `${c.name}Path`
191
- return ` ${c.name}: (() => {
192
- const cfg = {
193
- http,
194
- className: '${c.name}',
195
- relationMap: ${relMap},
196
- keyNames: ${kn},
197
- } as const;
198
- const api = makeDataClassApi<${c.name}>(cfg);
199
- return {
200
- all<S extends readonly ${pathsType}[] = readonly []>(
201
- options?: CollectionOptions & { select?: S },
202
- ): CollectionHandle<S['length'] extends 0 ? ${c.name} : SelectedEntity<${c.name}, S>> {
203
- const inner = api.all(options as CollectionOptions);
204
- return {
205
- ...inner,
206
- delete: inner.delete.bind(inner),
207
- release: inner.release.bind(inner),
208
- get length() {
209
- return inner.length;
210
- },
211
- [Symbol.asyncIterator]() {
212
- const it = inner[Symbol.asyncIterator]();
213
- return {
214
- async next() {
215
- const n = await it.next();
216
- if (!n.done && n.value) {
217
- map${c.name}Row(http, n.value);
218
- }
219
- return n;
220
- },
221
- };
222
- },
223
- } as CollectionHandle<S['length'] extends 0 ? ${c.name} : SelectedEntity<${c.name}, S>>;
224
- },
225
- async get<S extends readonly ${pathsType}[] = readonly []>(
226
- id: string | number,
227
- options?: { select?: S },
228
- ): Promise<(S['length'] extends 0 ? ${c.name} : SelectedEntity<${c.name}, S>) | null> {
229
- const entity = await api.get(id, options);
230
- if (entity) {
231
- map${c.name}Row(http, entity);
232
- }
233
- return entity as (S['length'] extends 0 ? ${c.name} : SelectedEntity<${c.name}, S>) | null;
234
- },
235
- delete: (id: string | number) => api.delete(id),
236
- query<S extends readonly ${pathsType}[] = readonly []>(
237
- filter: string,
238
- options?: CollectionOptions & { params?: unknown[]; select?: S },
239
- ): CollectionHandle<S['length'] extends 0 ? ${c.name} : SelectedEntity<${c.name}, S>> {
240
- const inner = api.query(filter, options as CollectionOptions);
241
- return {
242
- ...inner,
243
- delete: inner.delete.bind(inner),
244
- release: inner.release.bind(inner),
245
- get length() {
246
- return inner.length;
247
- },
248
- [Symbol.asyncIterator]() {
249
- const it = inner[Symbol.asyncIterator]();
250
- return {
251
- async next() {
252
- const n = await it.next();
253
- if (!n.done && n.value) {
254
- map${c.name}Row(http, n.value);
255
- }
256
- return n;
257
- },
258
- };
259
- },
260
- } as CollectionHandle<S['length'] extends 0 ? ${c.name} : SelectedEntity<${c.name}, S>>;
261
- },
262
- };
263
- })(),`
264
- })
265
- .join('\n')
266
-
267
- const authentifyBlock = hasAuthentify
268
- ? `
269
- authentify: {
270
- login: (body: { email: string; password: string }) =>
271
- callDatastorePath(http, ['authentify', 'login'], { body }),
272
- },`
273
- : ''
274
-
275
- const body = `
276
- export function createClient(config: QuadroClientConfig) {
277
- const http = new QuadroHttp({
278
- baseURL: config.baseURL,
279
- fetchImpl: config.fetchImpl,
280
- defaultHeaders: config.defaultHeaders,
281
- });
282
-
283
- ${mapFunctions}
284
-
285
- return {${authentifyBlock}
286
- ${classBranches}
287
- rpc: (segments: string[], init?: { method?: 'GET' | 'POST'; body?: unknown }) =>
288
- callDatastorePath(http, segments, init),
289
- sessionCookieName: quadrokitCatalogMeta.sessionCookieName,
290
- _http: http,
291
- };
292
- }
293
-
294
- export type QuadroClient = ReturnType<typeof createClient>;
295
- `
296
-
297
- return header + metaExport + configInterface + body
298
- }
299
-
300
- export async function writeGenerated(outDir: string, catalog: CatalogJson): Promise<void> {
301
- await mkdir(outDir, { recursive: true })
302
- await writeFile(path.join(outDir, 'types.gen.ts'), emitTypes(catalog), 'utf8')
303
- await writeFile(path.join(outDir, 'client.gen.ts'), emitClient(catalog), 'utf8')
304
- await writeFile(
305
- path.join(outDir, 'meta.json'),
306
- JSON.stringify(
307
- {
308
- __NAME: catalog.__NAME,
309
- sessionCookieName: sessionCookieName(catalog),
310
- generatedAt: new Date().toISOString(),
311
- },
312
- null,
313
- 2
314
- ),
315
- 'utf8'
316
- )
317
- }
@@ -1,135 +0,0 @@
1
- import type { QuadroHttp } from './http.js'
2
- import { buildListSearchParams, type ListQueryParams } from './query.js'
3
- import { unwrapEntityList } from './unwrap.js'
4
-
5
- export interface CollectionContext {
6
- http: QuadroHttp
7
- className: string
8
- relationMap: Record<string, string>
9
- /** Primary key attribute names (first wins for simple path). */
10
- keyNames: readonly string[]
11
- path: string
12
- /** Optional entity-set URL from a previous response for `release()`. */
13
- entitySetUrl?: string
14
- }
15
-
16
- export interface CollectionOptions extends ListQueryParams {
17
- page?: number
18
- pageSize?: number
19
- select?: readonly string[]
20
- }
21
-
22
- export type MapRow<R> = (raw: unknown) => R
23
-
24
- function primaryKeyFromRow(row: unknown, keyNames: readonly string[]): string | number | undefined {
25
- if (!row || typeof row !== 'object') {
26
- return undefined
27
- }
28
- const o = row as Record<string, unknown>
29
- for (const k of keyNames) {
30
- if (k in o && o[k] != null) {
31
- return o[k] as string | number
32
- }
33
- }
34
- return undefined
35
- }
36
-
37
- /**
38
- * Lazy paged collection: async iteration loads further pages; tracks PKs for `delete()`.
39
- */
40
- export function createCollection<R>(
41
- ctx: CollectionContext,
42
- initialOptions: CollectionOptions,
43
- mapRow: MapRow<R>
44
- ): CollectionHandle<R> {
45
- const seenIds = new Set<string | number>()
46
- let entitySetUrl = ctx.entitySetUrl
47
-
48
- async function fetchPage(page: number): Promise<unknown[]> {
49
- const qs = buildListSearchParams(ctx.className, { ...initialOptions, page }, ctx.relationMap)
50
- const res = await ctx.http.request(`${ctx.path}${qs}`)
51
- const text = await res.text()
52
- if (!res.ok) {
53
- throw new Error(`List failed ${res.status}: ${text.slice(0, 200)}`)
54
- }
55
- const loc = res.headers.get('Content-Location') ?? res.headers.get('Location')
56
- if (loc) {
57
- entitySetUrl = loc.startsWith('http')
58
- ? loc
59
- : `${ctx.http.baseURL}${loc.startsWith('/') ? '' : '/'}${loc}`
60
- }
61
- const json = text ? JSON.parse(text) : []
62
- return unwrapEntityList(ctx.className, json)
63
- }
64
-
65
- async function* iteratePages(): AsyncGenerator<R, void, undefined> {
66
- let page = initialOptions.page ?? 1
67
- const pageSize = initialOptions.pageSize ?? 50
68
- while (true) {
69
- const rows = await fetchPage(page)
70
- if (!rows.length) {
71
- break
72
- }
73
- for (const raw of rows) {
74
- const mapped = mapRow(raw)
75
- const pk = primaryKeyFromRow(raw, ctx.keyNames)
76
- if (pk !== undefined) {
77
- seenIds.add(pk)
78
- }
79
- yield mapped
80
- }
81
- if (rows.length < pageSize) {
82
- break
83
- }
84
- page += 1
85
- }
86
- }
87
-
88
- const iterable: AsyncIterable<R> = {
89
- [Symbol.asyncIterator]() {
90
- return iteratePages()[Symbol.asyncIterator]()
91
- },
92
- }
93
-
94
- const handle: CollectionHandle<R> = {
95
- ...iterable,
96
- async delete(): Promise<void> {
97
- const ids = [...seenIds]
98
- if (!ids.length) {
99
- return
100
- }
101
- await Promise.all(
102
- ids.map((id) =>
103
- ctx.http.void(`${ctx.className}(${encodeURIComponent(String(id))})`, {
104
- method: 'DELETE',
105
- })
106
- )
107
- )
108
- seenIds.clear()
109
- },
110
- async release(): Promise<void> {
111
- if (entitySetUrl) {
112
- try {
113
- await ctx.http.void(entitySetUrl.replace(ctx.http.baseURL, '') || entitySetUrl, {
114
- method: 'DELETE',
115
- })
116
- } catch {
117
- // Some servers ignore release; swallow
118
- }
119
- entitySetUrl = undefined
120
- }
121
- },
122
- get length(): number {
123
- return seenIds.size
124
- },
125
- }
126
-
127
- return handle
128
- }
129
-
130
- export type CollectionHandle<R> = AsyncIterable<R> & {
131
- delete(): Promise<void>
132
- release(): Promise<void>
133
- /** Number of entities seen while iterating (used after partial iteration). */
134
- readonly length: number
135
- }
@@ -1,156 +0,0 @@
1
- import { type CollectionHandle, type CollectionOptions, createCollection } from './collection.js'
2
- import type { QuadroHttp } from './http.js'
3
- import { buildEntityParams } from './query.js'
4
- import { unwrapEntity, unwrapEntityList } from './unwrap.js'
5
-
6
- export interface DataClassRuntimeConfig {
7
- http: QuadroHttp
8
- className: string
9
- /** relation attribute name → target class name for $expand */
10
- relationMap: Record<string, string>
11
- keyNames: readonly string[]
12
- }
13
-
14
- export function makeDataClassApi<R = unknown>(cfg: DataClassRuntimeConfig) {
15
- const path = `/${cfg.className}`
16
-
17
- return {
18
- all<S extends readonly string[] = readonly []>(
19
- options?: CollectionOptions & { select?: S }
20
- ): CollectionHandle<S extends readonly never[] ? R : R> {
21
- return createCollection(
22
- {
23
- http: cfg.http,
24
- className: cfg.className,
25
- relationMap: cfg.relationMap,
26
- keyNames: cfg.keyNames,
27
- path,
28
- },
29
- options ?? {},
30
- (raw) => raw as R
31
- )
32
- },
33
-
34
- async get<S extends readonly string[] = readonly []>(
35
- id: string | number,
36
- options?: { select?: S }
37
- ): Promise<R | null> {
38
- const qs = options?.select?.length
39
- ? buildEntityParams(cfg.className, options.select, cfg.relationMap)
40
- : ''
41
- const key = encodeURIComponent(String(id))
42
- const body = await cfg.http.json<unknown>(`${cfg.className}(${key})${qs}`)
43
- return unwrapEntity<R>(cfg.className, body) as R | null
44
- },
45
-
46
- async delete(id: string | number): Promise<boolean> {
47
- const key = encodeURIComponent(String(id))
48
- try {
49
- await cfg.http.void(`${cfg.className}(${key})`, { method: 'DELETE' })
50
- return true
51
- } catch {
52
- return false
53
- }
54
- },
55
-
56
- query<S extends readonly string[] = readonly []>(
57
- filter: string,
58
- options?: CollectionOptions & { params?: unknown[]; select?: S }
59
- ): CollectionHandle<S extends readonly never[] ? R : R> {
60
- const paramFilter = substituteQueryParams(filter, options?.params)
61
- return createCollection(
62
- {
63
- http: cfg.http,
64
- className: cfg.className,
65
- relationMap: cfg.relationMap,
66
- keyNames: cfg.keyNames,
67
- path,
68
- },
69
- { ...options, filter: paramFilter },
70
- (raw) => raw as R
71
- )
72
- },
73
- }
74
- }
75
-
76
- function substituteQueryParams(expr: string, params?: unknown[]): string {
77
- if (!params?.length) {
78
- return expr
79
- }
80
- let out = expr
81
- params.forEach((p, i) => {
82
- const placeholder = `:${i + 1}`
83
- const lit = formatFilterLiteral(p)
84
- out = out.split(placeholder).join(lit)
85
- })
86
- return out
87
- }
88
-
89
- function formatFilterLiteral(p: unknown): string {
90
- if (p === null || p === undefined) {
91
- return 'null'
92
- }
93
- if (typeof p === 'number') {
94
- return String(p)
95
- }
96
- if (typeof p === 'boolean') {
97
- return p ? 'true' : 'false'
98
- }
99
- const s = String(p).replace(/'/g, "''")
100
- return `'${s}'`
101
- }
102
-
103
- export interface EntityNavigationConfig {
104
- http: QuadroHttp
105
- parentClass: string
106
- parentId: string | number
107
- relationMap: Record<string, string>
108
- keyNames: Record<string, readonly string[]>
109
- }
110
-
111
- /** Nested collection under an entity, e.g. `Agency(1)/todayBookings`. */
112
- export function makeRelatedCollectionApi(
113
- cfg: EntityNavigationConfig,
114
- attributeName: string,
115
- targetClassName: string
116
- ) {
117
- const basePath = `/${cfg.parentClass}(${encodeURIComponent(String(cfg.parentId))})/${attributeName}`
118
-
119
- return {
120
- list<S extends readonly string[] = readonly []>(
121
- options?: CollectionOptions & { select?: S }
122
- ): CollectionHandle<unknown> {
123
- return createCollection(
124
- {
125
- http: cfg.http,
126
- className: targetClassName,
127
- relationMap: cfg.relationMap,
128
- keyNames: cfg.keyNames[targetClassName] ?? ['ID'],
129
- path: basePath,
130
- },
131
- options ?? {},
132
- (raw) => raw
133
- )
134
- },
135
- }
136
- }
137
-
138
- export function attachRelatedApis(
139
- row: unknown,
140
- cfg: EntityNavigationConfig,
141
- relations: readonly { attr: string; targetClass: string }[]
142
- ): void {
143
- if (!row || typeof row !== 'object') {
144
- return
145
- }
146
- const obj = row as Record<string, unknown>
147
- for (const { attr, targetClass } of relations) {
148
- Object.defineProperty(obj, attr, {
149
- enumerable: false,
150
- configurable: true,
151
- value: makeRelatedCollectionApi(cfg, attr, targetClass),
152
- })
153
- }
154
- }
155
-
156
- export { unwrapEntityList }
@@ -1,25 +0,0 @@
1
- import type { QuadroHttp } from './http.js'
2
-
3
- /** Call nested datastore paths like `authentify/login` (POST by default). */
4
- export async function callDatastorePath(
5
- http: QuadroHttp,
6
- segments: readonly string[],
7
- init?: { method?: 'GET' | 'POST'; body?: unknown }
8
- ): Promise<unknown> {
9
- const path = `/${segments.join('/')}`
10
- const method = init?.method ?? 'POST'
11
- if (method === 'GET') {
12
- return http.json(path, { method: 'GET' })
13
- }
14
- return http.json(path, {
15
- method: 'POST',
16
- body: init?.body !== undefined ? JSON.stringify(init.body) : undefined,
17
- })
18
- }
19
-
20
- export function createDatastoreNamespace(
21
- _http: QuadroHttp,
22
- tree: Record<string, unknown>
23
- ): Record<string, unknown> {
24
- return tree
25
- }
@@ -1,11 +0,0 @@
1
- export class QuadroHttpError extends Error {
2
- readonly status: number
3
- readonly body: string
4
-
5
- constructor(status: number, body: string) {
6
- super(`Quadro HTTP ${status}: ${body.slice(0, 200)}`)
7
- this.name = 'QuadroHttpError'
8
- this.status = status
9
- this.body = body
10
- }
11
- }
@@ -1,64 +0,0 @@
1
- import { QuadroHttpError } from './errors.js'
2
-
3
- export interface QuadroFetchOptions {
4
- baseURL: string
5
- fetchImpl?: typeof fetch
6
- /** Extra headers on every request (e.g. Authorization for generate). */
7
- defaultHeaders?: Record<string, string>
8
- }
9
-
10
- export function normalizeBaseURL(baseURL: string): string {
11
- return baseURL.replace(/\/$/, '')
12
- }
13
-
14
- export class QuadroHttp {
15
- constructor(private readonly opts: QuadroFetchOptions) {}
16
-
17
- get baseURL(): string {
18
- return normalizeBaseURL(this.opts.baseURL)
19
- }
20
-
21
- async request(path: string, init: RequestInit = {}): Promise<Response> {
22
- const url = path.startsWith('http')
23
- ? path
24
- : `${this.baseURL}${path.startsWith('/') ? '' : '/'}${path}`
25
- const headers = new Headers(init.headers)
26
- if (!headers.has('Accept')) {
27
- headers.set('Accept', 'application/json')
28
- }
29
- if (init.body !== undefined && !headers.has('Content-Type')) {
30
- headers.set('Content-Type', 'application/json')
31
- }
32
- for (const [k, v] of Object.entries(this.opts.defaultHeaders ?? {})) {
33
- if (!headers.has(k)) {
34
- headers.set(k, v)
35
- }
36
- }
37
- const fetchFn = this.opts.fetchImpl ?? fetch
38
- return fetchFn(url, {
39
- credentials: 'include',
40
- ...init,
41
- headers,
42
- })
43
- }
44
-
45
- async json<T>(path: string, init: RequestInit = {}): Promise<T> {
46
- const res = await this.request(path, init)
47
- const text = await res.text()
48
- if (!res.ok) {
49
- throw new QuadroHttpError(res.status, text)
50
- }
51
- if (!text) {
52
- return undefined as T
53
- }
54
- return JSON.parse(text) as T
55
- }
56
-
57
- async void(path: string, init: RequestInit = {}): Promise<void> {
58
- const res = await this.request(path, init)
59
- if (!res.ok) {
60
- const text = await res.text()
61
- throw new QuadroHttpError(res.status, text)
62
- }
63
- }
64
- }