@synthigy/sdk 0.1.1 → 0.1.2
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 +1 -2
- package/codegen/codegen.mjs +67 -18
- package/codegen/pull.mjs +7 -1
- package/package.json +1 -1
- package/src/index.d.ts +27 -0
- package/src/index.js +49 -4
package/README.md
CHANGED
|
@@ -79,8 +79,7 @@ for await (const ev of w.events) {
|
|
|
79
79
|
| [WATCH.md](./docs/WATCH.md) | `watch` / `watchQuery` / `watchSqlTemplate` / `observe`, lifecycle, and codegen |
|
|
80
80
|
| [AUTH.md](./docs/AUTH.md) | OAuth client registration, `actingAs`, BFF / SPA / native patterns |
|
|
81
81
|
| [CHANGELOG.md](./CHANGELOG.md) | Version history |
|
|
82
|
-
| [examples
|
|
83
|
-
| [examples/movies-live.mjs](./examples/movies-live.mjs) | Minimal live-watch demo |
|
|
82
|
+
| [synthigy/examples: movies/js](https://github.com/synthigy/examples/tree/main/movies/js) | End-to-end Datastar BFF against the published package |
|
|
84
83
|
| [src/index.d.ts](./src/index.d.ts) | Full type definitions |
|
|
85
84
|
|
|
86
85
|
## Tests
|
package/codegen/codegen.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { createHash } from 'node:crypto'
|
|
|
12
12
|
import { execFileSync } from 'node:child_process'
|
|
13
13
|
import { createRequire } from 'node:module'
|
|
14
14
|
import { TS, tsScalar, pascal, camel } from './lib.mjs'
|
|
15
|
+
import { PLATFORM_AUDIENCE } from '../src/index.js'
|
|
15
16
|
|
|
16
17
|
// typescript is an optional peer: the runtime client has no dependencies and
|
|
17
18
|
// must stay that way, but generating .ts files needs the compiler — resolved
|
|
@@ -56,6 +57,7 @@ Environment:
|
|
|
56
57
|
SYNTHIGY_ENDPOINT server URL (default: http://localhost:7887)
|
|
57
58
|
SYNTHIGY_CLIENT_ID OAuth client ID
|
|
58
59
|
SYNTHIGY_CLIENT_SECRET OAuth client secret
|
|
60
|
+
SYNTHIGY_AUDIENCE client_credentials audience, when the server requires one
|
|
59
61
|
|
|
60
62
|
Examples:
|
|
61
63
|
node codegen.mjs movies.xsql src/generated/ first run auto-pulls schema
|
|
@@ -136,7 +138,7 @@ const toKey = flags.keys === 'camel' ? camel : s => s
|
|
|
136
138
|
|
|
137
139
|
// ── describe ─────────────────────────────────────────────────────────────────
|
|
138
140
|
const ENDPOINT = process.env.SYNTHIGY_ENDPOINT ?? 'http://localhost:7887'
|
|
139
|
-
const { SYNTHIGY_CLIENT_ID, SYNTHIGY_CLIENT_SECRET } = process.env
|
|
141
|
+
const { SYNTHIGY_CLIENT_ID, SYNTHIGY_CLIENT_SECRET, SYNTHIGY_AUDIENCE } = process.env
|
|
140
142
|
|
|
141
143
|
// Auto-pull schema if missing or --pull requested (saved next to .xsql, cached for future runs).
|
|
142
144
|
if (!flags.schemaOverride && (flags.pull || !fs.existsSync(schemaPath))) {
|
|
@@ -156,7 +158,11 @@ async function token() {
|
|
|
156
158
|
method: 'POST',
|
|
157
159
|
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
158
160
|
body: new URLSearchParams({ grant_type: 'client_credentials',
|
|
159
|
-
client_id: SYNTHIGY_CLIENT_ID, client_secret: SYNTHIGY_CLIENT_SECRET
|
|
161
|
+
client_id: SYNTHIGY_CLIENT_ID, client_secret: SYNTHIGY_CLIENT_SECRET,
|
|
162
|
+
// describe is a platform-API call like any other, so it mints for the
|
|
163
|
+
// platform audience. Hand-rolled here rather than going through the
|
|
164
|
+
// client, so the default has to be named explicitly.
|
|
165
|
+
audience: SYNTHIGY_AUDIENCE || PLATFORM_AUDIENCE }),
|
|
160
166
|
})
|
|
161
167
|
if (!r.ok) throw new Error(`token ${r.status}: ${await r.text()}`)
|
|
162
168
|
return (await r.json()).access_token
|
|
@@ -206,7 +212,15 @@ if (cached && cached.sourceHash === sourceHash) {
|
|
|
206
212
|
if (!resp.ok) throw new Error(`describe ${resp.status}: ${await resp.text()}`)
|
|
207
213
|
const body = await resp.json()
|
|
208
214
|
const r0 = (body.results ?? body.result ?? [body.data ?? body])[0] ?? body
|
|
215
|
+
// A describe that fails answers 200 with {ok:false, error} in the result.
|
|
216
|
+
// Reading .operations off that yields "not iterable" and throws the
|
|
217
|
+
// server's diagnostic away — which is the only thing that says WHICH
|
|
218
|
+
// attribute the templates got wrong.
|
|
219
|
+
const err = r0?.error ?? (r0?.ok === false ? { message: 'describe failed' } : null)
|
|
220
|
+
if (err) throw new Error(err.message ?? JSON.stringify(err))
|
|
209
221
|
ir = r0.result ?? r0.data ?? r0
|
|
222
|
+
if (!Array.isArray(ir?.operations))
|
|
223
|
+
throw new Error(`describe returned no operations: ${JSON.stringify(ir).slice(0, 200)}`)
|
|
210
224
|
} catch (e) {
|
|
211
225
|
console.error(cached
|
|
212
226
|
? `sources changed since IR pull and describe failed (${e.message}) — start a backend (SYNTHIGY_ENDPOINT) or revert the .xsql edit`
|
|
@@ -251,7 +265,13 @@ fs.writeFileSync(irPath, JSON.stringify(ir, null, 2) + '\n')
|
|
|
251
265
|
// Identity (namespace / name), @watch, and sql-template @returns results all come
|
|
252
266
|
// from the IR now — the canonical XSQL parser owns them. No XSQL re-parsing here.
|
|
253
267
|
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'))
|
|
254
|
-
|
|
268
|
+
// Installed from npm -> import the package by name; a relative path out of
|
|
269
|
+
// node_modules is correct only for one install layout. Inside this repo the
|
|
270
|
+
// relative path is still what works, since there is no package to resolve.
|
|
271
|
+
const sdkRoot = path.join(here, '..')
|
|
272
|
+
const sdkSrc = sdkRoot.includes(`${path.sep}node_modules${path.sep}`)
|
|
273
|
+
? '@synthigy/sdk'
|
|
274
|
+
: path.relative(outDir, path.join(sdkRoot, 'src')) + '/index.js'
|
|
255
275
|
const ent = (n) => schema.entities[n] ?? {}
|
|
256
276
|
const visited = new Set()
|
|
257
277
|
|
|
@@ -341,7 +361,14 @@ for (const n of allEntities) nsOf(entityPascal(n))
|
|
|
341
361
|
// never emitted from XSQL. Skip LOUDLY, never silently.
|
|
342
362
|
const MUTATE_VERBS = new Set(['sync', 'stack', 'delete'])
|
|
343
363
|
for (const op of ir.operations) {
|
|
344
|
-
if (op.batch)
|
|
364
|
+
if (op.batch) {
|
|
365
|
+
// The Python and PHP generators emit a batch function; this one does not
|
|
366
|
+
// yet. Say so — the author declared an operation and would otherwise get
|
|
367
|
+
// nothing back with no diagnostic.
|
|
368
|
+
console.warn(`skipped @batch ${op.name} — not generated by the JS codegen yet; ` +
|
|
369
|
+
`call the members individually, or exec([op(...), op(...)]) for one round trip`)
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
345
372
|
if (MUTATE_VERBS.has(op.op)) {
|
|
346
373
|
console.warn(`skipped @${op.op} ${op.name} — mutations are generated from the schema; use ${entityPascal(op.entity ?? '<Entity>')}.${op.op}(data)`)
|
|
347
374
|
continue
|
|
@@ -490,20 +517,42 @@ fs.writeFileSync(path.join(outDir, 'ops.ts'), OPS_HDR_COMMENT + formatTs(OPS_
|
|
|
490
517
|
// type-check — never ship broken/stale output silently (the old emit-and-swallow
|
|
491
518
|
// shipped a stale ops.js once). No .js emitted: consumers import the .ts source
|
|
492
519
|
// (Node 23+ type-stripping / a bundler); the type-only `./schema.js` import erases
|
|
493
|
-
// at runtime.
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
path.join(
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
520
|
+
// at runtime.
|
|
521
|
+
// Locate tsc through the SAME resolution that found the `typescript` module
|
|
522
|
+
// above, so a hoisted install — the normal consumer layout — is found. A path
|
|
523
|
+
// built from `here` only exists when typescript is nested under this package.
|
|
524
|
+
const tscBin = (() => {
|
|
525
|
+
try {
|
|
526
|
+
const lib = createRequire(import.meta.url).resolve('typescript')
|
|
527
|
+
const bin = path.join(path.dirname(lib), '..', 'bin', 'tsc')
|
|
528
|
+
if (fs.existsSync(bin)) return bin
|
|
529
|
+
} catch {}
|
|
530
|
+
for (const c of [path.join(here, '..', 'node_modules', '.bin', 'tsc'),
|
|
531
|
+
path.join(process.cwd(), 'node_modules', '.bin', 'tsc')]) {
|
|
532
|
+
if (fs.existsSync(c)) return c
|
|
533
|
+
}
|
|
534
|
+
return null
|
|
535
|
+
})()
|
|
536
|
+
|
|
537
|
+
// `--ignoreConfig` needs TypeScript 6. On 5.x the flag is itself an error, and
|
|
538
|
+
// naming files explicitly already makes tsc skip tsconfig.json, so it is left off.
|
|
539
|
+
const tscMajor = Number.parseInt(ts.version ?? '0', 10) || 0
|
|
540
|
+
if (tscBin) {
|
|
541
|
+
try {
|
|
542
|
+
execFileSync(tscBin, [
|
|
543
|
+
...(tscMajor >= 6 ? ['--ignoreConfig'] : []),
|
|
544
|
+
'--module', 'nodenext', '--moduleResolution', 'nodenext',
|
|
545
|
+
'--target', 'es2022', '--strict', '--skipLibCheck', '--noEmit',
|
|
546
|
+
path.join(outDir, 'ops.ts'),
|
|
547
|
+
path.join(outDir, 'schema.ts'),
|
|
548
|
+
], { stdio: 'pipe' })
|
|
549
|
+
} catch (e) {
|
|
550
|
+
process.stderr.write('codegen: generated TS failed to type-check (not shipped):\n')
|
|
551
|
+
process.stderr.write((e.stdout ?? e.stderr ?? '').toString())
|
|
552
|
+
process.exit(1)
|
|
553
|
+
}
|
|
554
|
+
} else {
|
|
555
|
+
process.stderr.write('codegen: tsc not found — skipping the type-check gate.\n')
|
|
507
556
|
}
|
|
508
557
|
const flagSummary = [flags.keys !== 'snake' && `keys=${flags.keys}`, !flags.writes && 'no-writes'].filter(Boolean).join(' ')
|
|
509
558
|
const opCount = ir.operations.filter(o => !o.batch).length
|
package/codegen/pull.mjs
CHANGED
|
@@ -26,11 +26,17 @@ if (!endpoint) {
|
|
|
26
26
|
process.exit(1)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const { SYNTHIGY_TOKEN, SYNTHIGY_CLIENT_ID, SYNTHIGY_CLIENT_SECRET
|
|
29
|
+
const { SYNTHIGY_TOKEN, SYNTHIGY_CLIENT_ID, SYNTHIGY_CLIENT_SECRET,
|
|
30
|
+
SYNTHIGY_AUDIENCE } = process.env
|
|
30
31
|
const config = { endpoint }
|
|
31
32
|
if (SYNTHIGY_CLIENT_ID && SYNTHIGY_CLIENT_SECRET) {
|
|
32
33
|
config.clientId = SYNTHIGY_CLIENT_ID
|
|
33
34
|
config.clientSecret = SYNTHIGY_CLIENT_SECRET
|
|
35
|
+
// A server can require an explicit client_credentials audience for /data and
|
|
36
|
+
// /schema. Without it the mint still succeeds and the token is then refused,
|
|
37
|
+
// so the symptom is a 401 on schema rather than anything about auth config.
|
|
38
|
+
// A static SYNTHIGY_TOKEN already has its audience baked in.
|
|
39
|
+
if (SYNTHIGY_AUDIENCE) config.audience = SYNTHIGY_AUDIENCE
|
|
34
40
|
} else {
|
|
35
41
|
config.token = SYNTHIGY_TOKEN ?? '' // '' = authless (bare server)
|
|
36
42
|
}
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** The platform API this SDK is a client of; the default audience for every mint. */
|
|
2
|
+
export const PLATFORM_AUDIENCE: "https://synthigy.com"
|
|
3
|
+
|
|
1
4
|
// ============================================================================
|
|
2
5
|
// Selection
|
|
3
6
|
// ============================================================================
|
|
@@ -79,6 +82,13 @@ export interface TreeExecOpts extends ExecOpts {
|
|
|
79
82
|
childrenKey?: string
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
/** What `deploy()` acks with — `dataset` is the xid `destroy()` takes. */
|
|
86
|
+
export interface DeployAck {
|
|
87
|
+
deployed: boolean
|
|
88
|
+
version: string
|
|
89
|
+
dataset: string
|
|
90
|
+
}
|
|
91
|
+
|
|
82
92
|
export type OperationResult<T = unknown> =
|
|
83
93
|
| { ok: true; data: T }
|
|
84
94
|
| { ok: false; error: { message: string; code: string } }
|
|
@@ -608,6 +618,19 @@ export class SynthigyClient {
|
|
|
608
618
|
*/
|
|
609
619
|
deployedModel(opts?: ExecOpts): Promise<Record<string, unknown>>
|
|
610
620
|
|
|
621
|
+
/**
|
|
622
|
+
* Deploy a dataset version from a modeler export — pass the export file's
|
|
623
|
+
* contents verbatim, the server decodes it. Requires `dataset:deploy`.
|
|
624
|
+
* The ack carries the dataset xid `destroy()` takes.
|
|
625
|
+
*/
|
|
626
|
+
deploy(exportContents: string, opts?: ExecOpts): Promise<DeployAck>
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Destroy a dataset — every version, table and row. Requires
|
|
630
|
+
* `dataset:delete`. Idempotent.
|
|
631
|
+
*/
|
|
632
|
+
destroy(datasetXid: string, opts?: ExecOpts): Promise<boolean>
|
|
633
|
+
|
|
611
634
|
/**
|
|
612
635
|
* Fetch the runtime (enhanced) ERD model via `op: "runtime-model"` —
|
|
613
636
|
* the model as the running system actually emits it, with audit
|
|
@@ -1014,6 +1037,8 @@ export const op: {
|
|
|
1014
1037
|
sqlTemplate(template: string, params?: Record<string, unknown> | unknown[], opts?: { cached?: boolean }): Operation
|
|
1015
1038
|
deployedModel(): Operation
|
|
1016
1039
|
runtimeModel(): Operation
|
|
1040
|
+
deploy(exportContents: string): Operation
|
|
1041
|
+
destroy(datasetXid: string): Operation
|
|
1017
1042
|
}
|
|
1018
1043
|
|
|
1019
1044
|
// ============================================================================
|
|
@@ -1087,6 +1112,8 @@ export { _delete as delete }
|
|
|
1087
1112
|
|
|
1088
1113
|
export const schema: SynthigyClient['schema']
|
|
1089
1114
|
export const lint: SynthigyClient['lint']
|
|
1115
|
+
export const deploy: SynthigyClient['deploy']
|
|
1116
|
+
export const destroy: SynthigyClient['destroy']
|
|
1090
1117
|
export const deployedModel: SynthigyClient['deployedModel']
|
|
1091
1118
|
export const runtimeModel: SynthigyClient['runtimeModel']
|
|
1092
1119
|
export const token: SynthigyClient['token']
|
package/src/index.js
CHANGED
|
@@ -733,6 +733,15 @@ const RETRYABLE_CATEGORIES = new Set(['network', 'rate_limit', 'internal'])
|
|
|
733
733
|
* - .requestId— request ID for cross-correlation with server logs
|
|
734
734
|
* - .status — HTTP status code (for transport-level errors)
|
|
735
735
|
*/
|
|
736
|
+
/**
|
|
737
|
+
* The platform API this SDK is a client of. `/data`, `/schema`, `/history`,
|
|
738
|
+
* `/logs` and subscriptions all require a token bound to it. It identifies the
|
|
739
|
+
* API, never a deployment, so it is the same string on localhost and in
|
|
740
|
+
* production — which is exactly why it is a constant here rather than
|
|
741
|
+
* something every caller configures.
|
|
742
|
+
*/
|
|
743
|
+
export const PLATFORM_AUDIENCE = 'https://synthigy.com'
|
|
744
|
+
|
|
736
745
|
export class SynthigyError extends Error {
|
|
737
746
|
constructor(message, code, details, extras) {
|
|
738
747
|
super(message)
|
|
@@ -870,12 +879,16 @@ class SynthigyClient {
|
|
|
870
879
|
// [sub, client_id] isn't torn down between watches. Replaces the
|
|
871
880
|
// earlier "dummy keepalive watch" workaround.
|
|
872
881
|
this._keepAlive = config.keepAlive === true
|
|
873
|
-
//
|
|
874
|
-
//
|
|
875
|
-
//
|
|
882
|
+
// A client_credentials mint naming no audience resolves to the
|
|
883
|
+
// identity-only OIDC audience, which /data rejects. This SDK is the client
|
|
884
|
+
// for the platform API, so PLATFORM_AUDIENCE is what it asks for — there
|
|
885
|
+
// is nothing for a caller to configure, and nowhere to look the value up
|
|
886
|
+
// if there were: the server does not advertise it in discovery. Minting a
|
|
887
|
+
// token for some OTHER API is a per-call argument on token(), not a
|
|
888
|
+
// property of the client. The env var stays only as an escape hatch.
|
|
876
889
|
this._defaultAudience = config.audience
|
|
877
890
|
?? (typeof process !== 'undefined' ? process.env?.SYNTHIGY_AUDIENCE : undefined)
|
|
878
|
-
??
|
|
891
|
+
?? PLATFORM_AUDIENCE
|
|
879
892
|
|
|
880
893
|
if (config.token !== undefined && config.token !== null) {
|
|
881
894
|
// Static token mode — empty string allowed for unauth / dev servers.
|
|
@@ -1863,6 +1876,34 @@ class SynthigyClient {
|
|
|
1863
1876
|
return result.data
|
|
1864
1877
|
}
|
|
1865
1878
|
|
|
1879
|
+
/**
|
|
1880
|
+
* Deploy a dataset version from a modeler export. Pass the export file's
|
|
1881
|
+
* contents verbatim — the server decodes it. Requires `dataset:deploy`.
|
|
1882
|
+
*
|
|
1883
|
+
* @param {string} exportContents - Contents of a `<Dataset>@<version>.json` export
|
|
1884
|
+
* @param {Object} [opts] - Options ({ actingAs })
|
|
1885
|
+
* @returns {Promise<{deployed: boolean, version: string, dataset: string}>}
|
|
1886
|
+
*/
|
|
1887
|
+
async deploy(exportContents, opts) {
|
|
1888
|
+
const [result] = await this.exec([{ op: 'deploy', data: exportContents }], opts)
|
|
1889
|
+
if (!result.ok) throw _errorFromServer(result.error, undefined, result._requestId)
|
|
1890
|
+
return result.data
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
/**
|
|
1894
|
+
* Destroy a dataset: every version, every table, every row. Requires
|
|
1895
|
+
* `dataset:delete`. Idempotent — a dataset that is already gone is not an error.
|
|
1896
|
+
*
|
|
1897
|
+
* @param {string} datasetXid - The dataset xid (`deploy()` returns it)
|
|
1898
|
+
* @param {Object} [opts] - Options ({ actingAs })
|
|
1899
|
+
* @returns {Promise<boolean>}
|
|
1900
|
+
*/
|
|
1901
|
+
async destroy(datasetXid, opts) {
|
|
1902
|
+
const [result] = await this.exec([{ op: 'delete', entity: 'dataset', data: { xid: datasetXid } }], opts)
|
|
1903
|
+
if (!result.ok) throw _errorFromServer(result.error, undefined, result._requestId)
|
|
1904
|
+
return result.data
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1866
1907
|
/**
|
|
1867
1908
|
* Fetch the runtime ERD model — the deployed model augmented with:
|
|
1868
1909
|
* - `xid` / `euuid` identity attrs on every entity
|
|
@@ -2387,6 +2428,8 @@ export const op = {
|
|
|
2387
2428
|
sqlTemplate: (template, params) => ({ op: 'sql-template', template, params: params || [] }),
|
|
2388
2429
|
deployedModel: () => ({ op: 'deployed-model' }),
|
|
2389
2430
|
runtimeModel: () => ({ op: 'runtime-model' }),
|
|
2431
|
+
deploy: (exportContents) => ({ op: 'deploy', data: exportContents }),
|
|
2432
|
+
destroy: (datasetXid) => ({ op: 'delete', entity: 'dataset', data: { xid: datasetXid } }),
|
|
2390
2433
|
}
|
|
2391
2434
|
|
|
2392
2435
|
// ============================================================================
|
|
@@ -2512,6 +2555,8 @@ export const schema = (...a) => _dflt().schema(...a)
|
|
|
2512
2555
|
export const lint = (...a) => _dflt().lint(...a)
|
|
2513
2556
|
export const onboard = (...a) => _dflt().onboard(...a)
|
|
2514
2557
|
export const onboardComplete = (...a) => _dflt().onboardComplete(...a)
|
|
2558
|
+
export const deploy = (...a) => _dflt().deploy(...a)
|
|
2559
|
+
export const destroy = (...a) => _dflt().destroy(...a)
|
|
2515
2560
|
export const deployedModel = (...a) => _dflt().deployedModel(...a)
|
|
2516
2561
|
export const runtimeModel = (...a) => _dflt().runtimeModel(...a)
|
|
2517
2562
|
export const token = (...a) => _dflt().token(...a)
|