@zveltio/sdk 2.0.7 → 3.0.0-beta.1
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/dist/build/index.d.ts +77 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +157 -0
- package/dist/client/QueryBuilder.d.ts.map +1 -1
- package/dist/client/QueryBuilder.js +12 -3
- package/dist/client/RealtimeClient.d.ts.map +1 -1
- package/dist/client/RealtimeClient.js +3 -1
- package/dist/client/ZveltioClient.d.ts.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +16 -5
- package/dist/core.d.ts.map +1 -1
- package/dist/ddl/index.d.ts +153 -0
- package/dist/ddl/index.d.ts.map +1 -0
- package/dist/ddl/index.js +33 -0
- package/dist/extension/index.d.ts +522 -10
- package/dist/extension/index.d.ts.map +1 -1
- package/dist/extension/index.js +1 -1
- package/dist/extension/permission-gate.d.ts +37 -0
- package/dist/extension/permission-gate.d.ts.map +1 -0
- package/dist/extension/permission-gate.js +93 -0
- package/dist/generate-types.d.ts +37 -0
- package/dist/generate-types.d.ts.map +1 -0
- package/dist/generate-types.js +157 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/local-store.d.ts.map +1 -1
- package/dist/local-store.js +1 -3
- package/dist/offline/index.d.ts +105 -0
- package/dist/offline/index.d.ts.map +1 -0
- package/dist/offline/index.js +235 -0
- package/dist/publish/index.d.ts +94 -0
- package/dist/publish/index.d.ts.map +1 -0
- package/dist/publish/index.js +148 -0
- package/dist/realtime.d.ts.map +1 -1
- package/dist/realtime.js +6 -2
- package/dist/rpc/index.d.ts +68 -0
- package/dist/rpc/index.d.ts.map +1 -0
- package/dist/rpc/index.js +61 -0
- package/dist/schema-codegen.d.ts +60 -0
- package/dist/schema-codegen.d.ts.map +1 -0
- package/dist/schema-codegen.js +369 -0
- package/dist/schema-watcher.d.ts.map +1 -1
- package/dist/schema-watcher.js +1 -3
- package/dist/studio/index.d.ts +84 -0
- package/dist/studio/index.d.ts.map +1 -0
- package/dist/studio/index.js +180 -0
- package/dist/svelte.d.ts.map +1 -1
- package/dist/sync-manager.d.ts.map +1 -1
- package/dist/sync-manager.js +4 -11
- package/dist/testing/index.d.ts +121 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +283 -0
- package/dist/testing/with-test-db.d.ts +119 -0
- package/dist/testing/with-test-db.d.ts.map +1 -0
- package/dist/testing/with-test-db.js +313 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +3 -1
- package/dist/validate/index.d.ts +95 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +201 -0
- package/dist/validate/migration-parse.d.ts +17 -0
- package/dist/validate/migration-parse.d.ts.map +1 -0
- package/dist/validate/migration-parse.js +11 -0
- package/package.json +53 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio SDK surface (S3-02 + S3-03 + early S4-07 prep).
|
|
3
|
+
*
|
|
4
|
+
* Pure helpers for Studio extension authors:
|
|
5
|
+
* - `makeFormProxy` / `applyFormAlterHooks` — form-alter pipeline that
|
|
6
|
+
* `<Form>` renderers in core Studio call before render. Hooks add
|
|
7
|
+
* fields, hide fields, reorder, attach validators.
|
|
8
|
+
* - `sortSlotContributions` — stable priority-asc sort used by
|
|
9
|
+
* `<Slot name>` to order extension contributions.
|
|
10
|
+
*
|
|
11
|
+
* The reactive registries themselves (Svelte 5 `$state`) live in
|
|
12
|
+
* `packages/studio/src/lib/extension-api.svelte.ts`. They import this
|
|
13
|
+
* module so the algorithm is one source of truth, while the studio side
|
|
14
|
+
* owns the runtime state.
|
|
15
|
+
*
|
|
16
|
+
* Exported via `@zveltio/sdk/studio`. Extension Svelte bundles can
|
|
17
|
+
* import-and-call too — useful for advanced authors who want to apply
|
|
18
|
+
* alters against their own custom forms.
|
|
19
|
+
*/
|
|
20
|
+
export function makeFormProxy(initial) {
|
|
21
|
+
// Deep-copy fields so hooks can mutate without leaking to the original.
|
|
22
|
+
const fields = initial.fields.map((f) => ({
|
|
23
|
+
...f,
|
|
24
|
+
validators: [...(f.validators ?? [])],
|
|
25
|
+
}));
|
|
26
|
+
const hiddenSet = new Set();
|
|
27
|
+
return {
|
|
28
|
+
get fields() {
|
|
29
|
+
return fields;
|
|
30
|
+
},
|
|
31
|
+
addField({ after, before, field }) {
|
|
32
|
+
if (!field?.name) {
|
|
33
|
+
console.warn('[form-alter] addField: field.name is required');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (fields.some((f) => f.name === field.name)) {
|
|
37
|
+
console.warn(`[form-alter] addField: field "${field.name}" already exists — use mutate-by-name instead`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const target = after ?? before;
|
|
41
|
+
const cloned = { ...field, validators: [...(field.validators ?? [])] };
|
|
42
|
+
if (target) {
|
|
43
|
+
const idx = fields.findIndex((f) => f.name === target);
|
|
44
|
+
if (idx < 0) {
|
|
45
|
+
// Anchor not found → push to end. Warn so the author knows.
|
|
46
|
+
console.warn(`[form-alter] addField: anchor "${target}" not found; appending "${field.name}" at end`);
|
|
47
|
+
fields.push(cloned);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const at = after ? idx + 1 : idx;
|
|
51
|
+
fields.splice(at, 0, cloned);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
fields.push(cloned);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
hideField(name) {
|
|
59
|
+
hiddenSet.add(name);
|
|
60
|
+
},
|
|
61
|
+
reorder(order) {
|
|
62
|
+
// Reorder only what's in `order`. Fields not mentioned keep their
|
|
63
|
+
// original relative order, appended after the reordered block.
|
|
64
|
+
const byName = new Map(fields.map((f) => [f.name, f]));
|
|
65
|
+
const reorderedKnown = order.map((n) => byName.get(n)).filter(Boolean);
|
|
66
|
+
const remaining = fields.filter((f) => !order.includes(f.name));
|
|
67
|
+
fields.length = 0;
|
|
68
|
+
fields.push(...reorderedKnown, ...remaining);
|
|
69
|
+
},
|
|
70
|
+
addValidator(fieldName, validator) {
|
|
71
|
+
const f = fields.find((x) => x.name === fieldName);
|
|
72
|
+
if (!f) {
|
|
73
|
+
console.warn(`[form-alter] addValidator: field "${fieldName}" not found`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
f.validators = [...(f.validators ?? []), validator];
|
|
77
|
+
},
|
|
78
|
+
commit() {
|
|
79
|
+
// Materialize hidden state. `hidden: true` keeps the field in the
|
|
80
|
+
// schema so server-side defaults still apply; renderers skip rendering.
|
|
81
|
+
const out = fields.map((f) => (hiddenSet.has(f.name) ? { ...f, hidden: true } : f));
|
|
82
|
+
return { ...initial, fields: out };
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Run every registered hook against the schema and return the result.
|
|
88
|
+
* Exception-safe: a throwing hook is logged and skipped, the rest still run.
|
|
89
|
+
*/
|
|
90
|
+
export function applyFormAlterHooks(hooks, schema, ctx = {}) {
|
|
91
|
+
if (hooks.length === 0)
|
|
92
|
+
return schema;
|
|
93
|
+
const proxy = makeFormProxy(schema);
|
|
94
|
+
for (const hook of hooks) {
|
|
95
|
+
try {
|
|
96
|
+
hook(proxy, ctx);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.error('[form-alter] hook threw:', err);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return proxy.commit();
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Sort slot contributions deterministically: priority asc (lower first),
|
|
106
|
+
* then registration order (stable sort).
|
|
107
|
+
*/
|
|
108
|
+
export function sortSlotContributions(items) {
|
|
109
|
+
// We rely on Array.prototype.sort being stable (TC39 since 2019).
|
|
110
|
+
return items.slice().sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
|
|
111
|
+
}
|
|
112
|
+
function getStudioGlobal() {
|
|
113
|
+
if (typeof window === 'undefined')
|
|
114
|
+
return null;
|
|
115
|
+
const g = window.__zveltio;
|
|
116
|
+
if (!g) {
|
|
117
|
+
console.warn('[zveltio/sdk/studio] window.__zveltio is not installed. Is the bundle running inside Studio?');
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return g;
|
|
121
|
+
}
|
|
122
|
+
/** Register a top-level Studio page contributed by this extension. */
|
|
123
|
+
export function registerRoute(route) {
|
|
124
|
+
getStudioGlobal()?.registerRoute(route);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Register a Svelte component into a named composition slot (S3-03).
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* import { registerSlot } from '@zveltio/sdk/studio';
|
|
131
|
+
* import RevenueWidget from './widgets/RevenueWidget.svelte';
|
|
132
|
+
*
|
|
133
|
+
* registerSlot('dashboard.widgets', {
|
|
134
|
+
* component: RevenueWidget,
|
|
135
|
+
* priority: 10,
|
|
136
|
+
* visible: (ctx) => (ctx.user as any)?.roles?.includes('finance'),
|
|
137
|
+
* });
|
|
138
|
+
*/
|
|
139
|
+
export function registerSlot(name, contribution) {
|
|
140
|
+
getStudioGlobal()?.registerSlot(name, contribution);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Register a Drupal-style form-alter hook (S3-02).
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* import { registerFormAlter } from '@zveltio/sdk/studio';
|
|
147
|
+
*
|
|
148
|
+
* registerFormAlter('core:user-edit', (form) => {
|
|
149
|
+
* form.addField({ after: 'email', field: { name: 'phone', type: 'tel' } });
|
|
150
|
+
* form.hideField('legacy_pin');
|
|
151
|
+
* });
|
|
152
|
+
*/
|
|
153
|
+
export function registerFormAlter(formId, hook) {
|
|
154
|
+
getStudioGlobal()?.registerFormAlter(formId, hook);
|
|
155
|
+
}
|
|
156
|
+
/** Register a custom Studio field type contributed by this extension. */
|
|
157
|
+
export function registerFieldType(ft) {
|
|
158
|
+
getStudioGlobal()?.registerFieldType(ft);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Register a custom preview handler for asset URLs / MIME types.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* import { registerAssetPreview } from '@zveltio/sdk/studio';
|
|
165
|
+
* import PdfPreview from './PdfPreview.svelte';
|
|
166
|
+
*
|
|
167
|
+
* registerAssetPreview({
|
|
168
|
+
* match: (a) => a.mimeType === 'application/pdf',
|
|
169
|
+
* component: PdfPreview,
|
|
170
|
+
* });
|
|
171
|
+
*/
|
|
172
|
+
export function registerAssetPreview(handler) {
|
|
173
|
+
getStudioGlobal()?.registerAssetPreview(handler);
|
|
174
|
+
}
|
|
175
|
+
/** Engine base URL — pre-installed by Studio for cross-origin fetches. */
|
|
176
|
+
export function getEngineUrl() {
|
|
177
|
+
if (typeof window === 'undefined')
|
|
178
|
+
return '';
|
|
179
|
+
return window.__ZVELTIO_ENGINE_URL__ || getStudioGlobal()?.engineUrl || '';
|
|
180
|
+
}
|
package/dist/svelte.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"svelte.d.ts","sourceRoot":"","sources":["../src/svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,WAAW,EACjB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,IAAI,GAC/B,MAAM,IAAI,CAGZ;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,CAAC,MAAM,EAAE;
|
|
1
|
+
{"version":3,"file":"svelte.d.ts","sourceRoot":"","sources":["../src/svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,WAAW,EACjB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,IAAI,GAC/B,MAAM,IAAI,CAGZ;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,KAAK,IAAI,EACnF,UAAU,SAAO,GAChB,MAAM,IAAI,CAeZ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-manager.d.ts","sourceRoot":"","sources":["../src/sync-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAMjD,MAAM,MAAM,sBAAsB,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,KAAK,GAAG,CAAC;IAC9C,mHAAmH;IACnH,kBAAkB,CAAC,EAAE,sBAAsB,CAAC;CAC7C;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,SAAS,CAAyD;gBAE9D,MAAM,EAAE,aAAa,EAAE,MAAM,GAAE,iBAAsB;IAY3D,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"sync-manager.d.ts","sourceRoot":"","sources":["../src/sync-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAMjD,MAAM,MAAM,sBAAsB,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,KAAK,GAAG,CAAC;IAC9C,mHAAmH;IACnH,kBAAkB,CAAC,EAAE,sBAAsB,CAAC;CAC7C;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,SAAS,CAAyD;gBAE9D,MAAM,EAAE,aAAa,EAAE,MAAM,GAAE,iBAAsB;IAY3D,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BhD;;;;;OAKG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM;QAEnB,2CAA2C;;;;;QAU3C,6CAA6C;kBAC7B,MAAM;;;;QAUtB,0CAA0C;uBACrB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;;;QAYxC,0CAA0C;qBACvB,MAAM,QAAQ,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;;;QAapD,+CAA+C;qBAC5B,MAAM;QAMzB,qDAAqD;8BAC/B,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,IAAI;QA0C9C,8CAA8C;;QAK9C,kCAAkC;8BACN,MAAM,gBAAgB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;IAQzE,oCAAoC;IAC9B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YA8FhB,eAAe;IAmB7B,wDAAwD;IAClD,SAAS,IAAI,OAAO,CAAC;QACzB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,OAAO,CAAC;KACnB,CAAC;IAUI,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAK5B"}
|
package/dist/sync-manager.js
CHANGED
|
@@ -115,12 +115,9 @@ export class SyncManager {
|
|
|
115
115
|
if (this.realtime) {
|
|
116
116
|
unsubRealtime = this.realtime.subscribe(name, async (event) => {
|
|
117
117
|
// Apply update from server to local store
|
|
118
|
-
if (event.event === 'record.created' ||
|
|
119
|
-
event.event === 'record.updated') {
|
|
118
|
+
if (event.event === 'record.created' || event.event === 'record.updated') {
|
|
120
119
|
try {
|
|
121
|
-
const serverRecord = await this.client
|
|
122
|
-
.collection(name)
|
|
123
|
-
.get(event.record_id);
|
|
120
|
+
const serverRecord = await this.client.collection(name).get(event.record_id);
|
|
124
121
|
await this.store.applyServerUpdate(name, event.record_id, serverRecord, Date.now());
|
|
125
122
|
this.notifyListeners(name);
|
|
126
123
|
}
|
|
@@ -209,9 +206,7 @@ export class SyncManager {
|
|
|
209
206
|
.create({ id: op.recordId, ...crdtPayload });
|
|
210
207
|
break;
|
|
211
208
|
case 'update':
|
|
212
|
-
await this.client
|
|
213
|
-
.collection(op.collection)
|
|
214
|
-
.update(op.recordId, crdtPayload);
|
|
209
|
+
await this.client.collection(op.collection).update(op.recordId, crdtPayload);
|
|
215
210
|
break;
|
|
216
211
|
case 'delete':
|
|
217
212
|
await this.client.collection(op.collection).delete(op.recordId);
|
|
@@ -224,9 +219,7 @@ export class SyncManager {
|
|
|
224
219
|
// Conflict from server (409) — apply conflict resolution
|
|
225
220
|
if (err.message?.includes('409')) {
|
|
226
221
|
try {
|
|
227
|
-
const serverRecord = await this.client
|
|
228
|
-
.collection(op.collection)
|
|
229
|
-
.get(op.recordId);
|
|
222
|
+
const serverRecord = await this.client.collection(op.collection).get(op.recordId);
|
|
230
223
|
const localRecord = await this.store.get(op.collection, op.recordId);
|
|
231
224
|
// CRDT LWW merge if both sides have CRDT docs and strategy is 'lww'
|
|
232
225
|
if (this.config.conflictResolution === 'lww' &&
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testing helpers for Zveltio extensions (S4-06).
|
|
3
|
+
*
|
|
4
|
+
* Goal: give extension authors a one-liner to spin up a fake
|
|
5
|
+
* `ExtensionContext` and a Hono test app, so unit tests run without a
|
|
6
|
+
* real database or auth setup. For integration tests against a real
|
|
7
|
+
* Postgres + engine, use Bun's `testcontainers` directly — this module
|
|
8
|
+
* is the offline-fast path.
|
|
9
|
+
*
|
|
10
|
+
* Three primitives:
|
|
11
|
+
* - `createTestContext(overrides?)` — minimal `ExtensionContext` with
|
|
12
|
+
* no-op stubs for everything (db, auth, events, services,
|
|
13
|
+
* queryAlter, entityAccess, registerPublicRoute). Override per-test.
|
|
14
|
+
* - `createTestApp(extension, ctx?)` — runs `extension.register()`
|
|
15
|
+
* against a fresh Hono and returns the app, ready for `.request()`.
|
|
16
|
+
* - `mockDb(presets?)` — records query-builder calls; returns presets
|
|
17
|
+
* when an `.execute()` / `.executeTakeFirst()` lands. Enough to
|
|
18
|
+
* fake CRUD paths; falls back to empty arrays / undefined.
|
|
19
|
+
*/
|
|
20
|
+
import { Hono } from 'hono';
|
|
21
|
+
import type { ZveltioExtension, ExtensionContext } from '../extension/index.js';
|
|
22
|
+
export interface MockCall {
|
|
23
|
+
/** Method name chain, e.g. `'selectFrom.where.execute'`. */
|
|
24
|
+
chain: string;
|
|
25
|
+
/** Args of the terminal call. */
|
|
26
|
+
args: unknown[];
|
|
27
|
+
}
|
|
28
|
+
export interface MockDbPresets {
|
|
29
|
+
/**
|
|
30
|
+
* Preset a return value for a specific chain. Match is by suffix —
|
|
31
|
+
* `'selectFrom.zvd_users.execute'` matches the chain ending in that.
|
|
32
|
+
* Use exact chains for precision.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* mockDb({
|
|
36
|
+
* 'selectFrom.zvd_users.selectAll.execute': [{ id: '1', name: 'A' }],
|
|
37
|
+
* 'selectFrom.zvd_users.executeTakeFirst': { id: '1', name: 'A' },
|
|
38
|
+
* })
|
|
39
|
+
*/
|
|
40
|
+
[chain: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
export interface MockDb {
|
|
43
|
+
/** Recorded calls, oldest first. Reset between tests. */
|
|
44
|
+
readonly calls: MockCall[];
|
|
45
|
+
/** Reset call history (presets stay). */
|
|
46
|
+
reset(): void;
|
|
47
|
+
/** Add or override a preset after construction. */
|
|
48
|
+
preset(chain: string, value: unknown): void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build a recording mock Kysely-shaped object. Any method chain is
|
|
52
|
+
* accepted; the terminal `.execute()` / `.executeTakeFirst()` /
|
|
53
|
+
* `.executeTakeFirstOrThrow()` returns a preset if matched, otherwise
|
|
54
|
+
* an empty list (for execute) or undefined.
|
|
55
|
+
*/
|
|
56
|
+
export declare function mockDb(presets?: MockDbPresets): any & MockDb;
|
|
57
|
+
export interface MockEventBus {
|
|
58
|
+
emit(event: string, payload: unknown): void;
|
|
59
|
+
on(event: string, handler: (payload: any) => void): () => void;
|
|
60
|
+
onBefore(event: string, handler: (payload: any) => any): () => void;
|
|
61
|
+
runBefore(event: string, seed: Record<string, unknown>): Promise<any>;
|
|
62
|
+
/** Recorded emits, oldest first. */
|
|
63
|
+
readonly emitted: Array<{
|
|
64
|
+
event: string;
|
|
65
|
+
payload: unknown;
|
|
66
|
+
}>;
|
|
67
|
+
/** Reset emit log. */
|
|
68
|
+
reset(): void;
|
|
69
|
+
}
|
|
70
|
+
export declare function mockEventBus(): MockEventBus;
|
|
71
|
+
export declare function mockServiceRegistry(): {
|
|
72
|
+
register(name: string, value: unknown): void;
|
|
73
|
+
unregister(name: string): void;
|
|
74
|
+
get<T = unknown>(name: string): T | null;
|
|
75
|
+
has(name: string): boolean;
|
|
76
|
+
list(): string[];
|
|
77
|
+
waitFor<T = unknown>(name: string, timeoutMs?: number): Promise<T>;
|
|
78
|
+
};
|
|
79
|
+
export interface MockAuthOptions {
|
|
80
|
+
/** If set, getSession returns `{ user: this }`. If null, getSession returns null. */
|
|
81
|
+
user?: {
|
|
82
|
+
id: string;
|
|
83
|
+
email?: string;
|
|
84
|
+
name?: string;
|
|
85
|
+
roles?: string[];
|
|
86
|
+
} | null;
|
|
87
|
+
}
|
|
88
|
+
export declare function mockAuth(opts?: MockAuthOptions): any;
|
|
89
|
+
export interface CreateTestContextOptions {
|
|
90
|
+
/** Override the mocked db. Default: `mockDb()`. */
|
|
91
|
+
db?: any;
|
|
92
|
+
/** Override the mocked auth. Default: signed-in test user. */
|
|
93
|
+
auth?: any;
|
|
94
|
+
/** Override the mocked event bus. Default: in-memory. */
|
|
95
|
+
events?: MockEventBus;
|
|
96
|
+
/** Provide additional fields. They go onto the returned ctx as-is. */
|
|
97
|
+
extra?: Partial<ExtensionContext<any>>;
|
|
98
|
+
}
|
|
99
|
+
export declare function createTestContext(opts?: CreateTestContextOptions): ExtensionContext<any>;
|
|
100
|
+
export interface CreateTestAppOptions {
|
|
101
|
+
/** Override the context handed to register(). Default: `createTestContext()`. */
|
|
102
|
+
ctx?: ExtensionContext<any>;
|
|
103
|
+
/**
|
|
104
|
+
* When the extension uses `mountStrategy: 'subapp'`, the engine normally
|
|
105
|
+
* mounts the sub-app at `/ext/<name>`. For tests we replicate that so
|
|
106
|
+
* `.request('/ext/<name>/...')` works. Pass `false` to skip the wrapper
|
|
107
|
+
* and mount the sub-app at root for cleaner test URLs.
|
|
108
|
+
*/
|
|
109
|
+
mountSubappAt?: string | false;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Spin up a Hono test app with the extension's routes registered. Returns
|
|
113
|
+
* the outer Hono so callers can use `.request(path, init)` for assertions.
|
|
114
|
+
*
|
|
115
|
+
* Honors the extension's `mountStrategy`. For `'subapp'`, the outer app
|
|
116
|
+
* mounts the sub-app at `/ext/<extension.name>` by default — same shape
|
|
117
|
+
* the engine produces in production.
|
|
118
|
+
*/
|
|
119
|
+
export declare function createTestApp(extension: ZveltioExtension<any>, opts?: CreateTestAppOptions): Promise<Hono>;
|
|
120
|
+
export { withTestDb, startTestDb, applyMigrationStrings, applyMigrationFiles, stopReusedTestDb, splitStatements, type TestDb, type WithTestDbOptions, } from './with-test-db.js';
|
|
121
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testing helpers for Zveltio extensions (S4-06).
|
|
3
|
+
*
|
|
4
|
+
* Goal: give extension authors a one-liner to spin up a fake
|
|
5
|
+
* `ExtensionContext` and a Hono test app, so unit tests run without a
|
|
6
|
+
* real database or auth setup. For integration tests against a real
|
|
7
|
+
* Postgres + engine, use Bun's `testcontainers` directly — this module
|
|
8
|
+
* is the offline-fast path.
|
|
9
|
+
*
|
|
10
|
+
* Three primitives:
|
|
11
|
+
* - `createTestContext(overrides?)` — minimal `ExtensionContext` with
|
|
12
|
+
* no-op stubs for everything (db, auth, events, services,
|
|
13
|
+
* queryAlter, entityAccess, registerPublicRoute). Override per-test.
|
|
14
|
+
* - `createTestApp(extension, ctx?)` — runs `extension.register()`
|
|
15
|
+
* against a fresh Hono and returns the app, ready for `.request()`.
|
|
16
|
+
* - `mockDb(presets?)` — records query-builder calls; returns presets
|
|
17
|
+
* when an `.execute()` / `.executeTakeFirst()` lands. Enough to
|
|
18
|
+
* fake CRUD paths; falls back to empty arrays / undefined.
|
|
19
|
+
*/
|
|
20
|
+
import { Hono } from 'hono';
|
|
21
|
+
/**
|
|
22
|
+
* Build a recording mock Kysely-shaped object. Any method chain is
|
|
23
|
+
* accepted; the terminal `.execute()` / `.executeTakeFirst()` /
|
|
24
|
+
* `.executeTakeFirstOrThrow()` returns a preset if matched, otherwise
|
|
25
|
+
* an empty list (for execute) or undefined.
|
|
26
|
+
*/
|
|
27
|
+
export function mockDb(presets = {}) {
|
|
28
|
+
const calls = [];
|
|
29
|
+
const presetMap = new Map(Object.entries(presets));
|
|
30
|
+
function buildChain(prefix) {
|
|
31
|
+
const proxy = new Proxy(() => { }, {
|
|
32
|
+
get(_, prop) {
|
|
33
|
+
if (typeof prop === 'symbol')
|
|
34
|
+
return undefined;
|
|
35
|
+
if (prop === 'calls')
|
|
36
|
+
return calls;
|
|
37
|
+
if (prop === 'reset')
|
|
38
|
+
return () => {
|
|
39
|
+
calls.length = 0;
|
|
40
|
+
};
|
|
41
|
+
if (prop === 'preset')
|
|
42
|
+
return (chain, value) => {
|
|
43
|
+
presetMap.set(chain, value);
|
|
44
|
+
};
|
|
45
|
+
if (prop === 'then')
|
|
46
|
+
return undefined;
|
|
47
|
+
// Build next link in the chain.
|
|
48
|
+
return buildChain([...prefix, prop]);
|
|
49
|
+
},
|
|
50
|
+
// NOTE: synchronous apply. Terminal calls return a Promise; non-
|
|
51
|
+
// terminal calls return the proxy itself so further chaining works
|
|
52
|
+
// without awaiting. (Async apply would wrap every step in a Promise
|
|
53
|
+
// and break `.selectFrom().selectAll().execute()` chains.)
|
|
54
|
+
apply(_t, _this, args) {
|
|
55
|
+
const chain = prefix.join('.');
|
|
56
|
+
calls.push({ chain, args });
|
|
57
|
+
const terminal = prefix[prefix.length - 1];
|
|
58
|
+
const isTerminal = terminal === 'execute' ||
|
|
59
|
+
terminal === 'executeTakeFirst' ||
|
|
60
|
+
terminal === 'executeTakeFirstOrThrow';
|
|
61
|
+
if (isTerminal) {
|
|
62
|
+
const preset = matchPreset(chain, presetMap);
|
|
63
|
+
if (preset !== undefined) {
|
|
64
|
+
const resolved = typeof preset === 'function' ? preset(...args) : preset;
|
|
65
|
+
return Promise.resolve(resolved);
|
|
66
|
+
}
|
|
67
|
+
if (terminal === 'execute')
|
|
68
|
+
return Promise.resolve([]);
|
|
69
|
+
if (terminal === 'executeTakeFirst')
|
|
70
|
+
return Promise.resolve(undefined);
|
|
71
|
+
// executeTakeFirstOrThrow with no preset → reject
|
|
72
|
+
return Promise.reject(new Error(`mockDb: no preset for chain "${chain}" and executeTakeFirstOrThrow has no fallback`));
|
|
73
|
+
}
|
|
74
|
+
// Non-terminal: keep chaining on the same proxy (its prefix is
|
|
75
|
+
// unchanged; the next property access will extend via get()).
|
|
76
|
+
return proxy;
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
return proxy;
|
|
80
|
+
}
|
|
81
|
+
return buildChain([]);
|
|
82
|
+
}
|
|
83
|
+
function matchPreset(chain, presets) {
|
|
84
|
+
// Exact match wins.
|
|
85
|
+
if (presets.has(chain))
|
|
86
|
+
return presets.get(chain);
|
|
87
|
+
// Suffix match: preset 'foo.execute' matches 'selectFrom.x.foo.execute'.
|
|
88
|
+
for (const [key, val] of presets) {
|
|
89
|
+
if (chain.endsWith(key))
|
|
90
|
+
return val;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
export function mockEventBus() {
|
|
95
|
+
const emitted = [];
|
|
96
|
+
const listeners = new Map();
|
|
97
|
+
const beforeHandlers = new Map();
|
|
98
|
+
return {
|
|
99
|
+
emit(event, payload) {
|
|
100
|
+
emitted.push({ event, payload });
|
|
101
|
+
for (const h of listeners.get(event) ?? [])
|
|
102
|
+
h(payload);
|
|
103
|
+
},
|
|
104
|
+
on(event, handler) {
|
|
105
|
+
const list = listeners.get(event) ?? [];
|
|
106
|
+
list.push(handler);
|
|
107
|
+
listeners.set(event, list);
|
|
108
|
+
return () => {
|
|
109
|
+
const cur = listeners.get(event);
|
|
110
|
+
if (cur) {
|
|
111
|
+
const idx = cur.indexOf(handler);
|
|
112
|
+
if (idx >= 0)
|
|
113
|
+
cur.splice(idx, 1);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
onBefore(event, handler) {
|
|
118
|
+
const list = beforeHandlers.get(event) ?? [];
|
|
119
|
+
list.push(handler);
|
|
120
|
+
beforeHandlers.set(event, list);
|
|
121
|
+
return () => {
|
|
122
|
+
const cur = beforeHandlers.get(event);
|
|
123
|
+
if (cur) {
|
|
124
|
+
const idx = cur.indexOf(handler);
|
|
125
|
+
if (idx >= 0)
|
|
126
|
+
cur.splice(idx, 1);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
async runBefore(event, seed) {
|
|
131
|
+
const payload = { ...seed };
|
|
132
|
+
let aborted = false;
|
|
133
|
+
let abortReason = '';
|
|
134
|
+
payload.abort = (reason) => {
|
|
135
|
+
aborted = true;
|
|
136
|
+
abortReason = reason;
|
|
137
|
+
const err = new Error(`Aborted: ${reason}`);
|
|
138
|
+
err.name = 'AbortHookError';
|
|
139
|
+
err.reason = reason;
|
|
140
|
+
throw err;
|
|
141
|
+
};
|
|
142
|
+
payload.mutate = (patch) => {
|
|
143
|
+
const key = event === 'record.beforeUpdate' ? 'patch' : 'data';
|
|
144
|
+
payload[key] = { ...payload[key], ...patch };
|
|
145
|
+
};
|
|
146
|
+
for (const h of beforeHandlers.get(event) ?? []) {
|
|
147
|
+
await h(payload);
|
|
148
|
+
}
|
|
149
|
+
return payload;
|
|
150
|
+
},
|
|
151
|
+
get emitted() {
|
|
152
|
+
return emitted;
|
|
153
|
+
},
|
|
154
|
+
reset() {
|
|
155
|
+
emitted.length = 0;
|
|
156
|
+
listeners.clear();
|
|
157
|
+
beforeHandlers.clear();
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// ─── Mock service registry ─────────────────────────────────────────────────
|
|
162
|
+
export function mockServiceRegistry() {
|
|
163
|
+
const services = new Map();
|
|
164
|
+
return {
|
|
165
|
+
register(name, value) {
|
|
166
|
+
services.set(name, value);
|
|
167
|
+
},
|
|
168
|
+
unregister(name) {
|
|
169
|
+
services.delete(name);
|
|
170
|
+
},
|
|
171
|
+
get(name) {
|
|
172
|
+
return services.get(name) ?? null;
|
|
173
|
+
},
|
|
174
|
+
has(name) {
|
|
175
|
+
return services.has(name);
|
|
176
|
+
},
|
|
177
|
+
list() {
|
|
178
|
+
return [...services.keys()];
|
|
179
|
+
},
|
|
180
|
+
async waitFor(name) {
|
|
181
|
+
const v = services.get(name);
|
|
182
|
+
if (v === undefined)
|
|
183
|
+
throw new Error(`mockServiceRegistry: no service "${name}" registered`);
|
|
184
|
+
return v;
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// ─── Mock queryAlter / entityAccess scoped registries ──────────────────────
|
|
189
|
+
function mockQueryAlterScope() {
|
|
190
|
+
const entries = [];
|
|
191
|
+
return {
|
|
192
|
+
register(def) {
|
|
193
|
+
entries.push(def);
|
|
194
|
+
},
|
|
195
|
+
list() {
|
|
196
|
+
return entries.map((e) => ({ table: e.table }));
|
|
197
|
+
},
|
|
198
|
+
unregisterAll() {
|
|
199
|
+
entries.length = 0;
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function mockEntityAccessScope() {
|
|
204
|
+
const entries = [];
|
|
205
|
+
return {
|
|
206
|
+
register(def) {
|
|
207
|
+
entries.push(def);
|
|
208
|
+
},
|
|
209
|
+
list() {
|
|
210
|
+
return entries.map((e) => ({ table: e.table }));
|
|
211
|
+
},
|
|
212
|
+
unregisterAll() {
|
|
213
|
+
entries.length = 0;
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
export function mockAuth(opts = {}) {
|
|
218
|
+
const user = opts.user === undefined ? { id: 'test-user', email: 'test@example.com', roles: [] } : opts.user;
|
|
219
|
+
return {
|
|
220
|
+
api: {
|
|
221
|
+
async getSession() {
|
|
222
|
+
return user === null ? null : { user };
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
export function createTestContext(opts = {}) {
|
|
228
|
+
const db = opts.db ?? mockDb();
|
|
229
|
+
const events = opts.events ?? mockEventBus();
|
|
230
|
+
return {
|
|
231
|
+
db,
|
|
232
|
+
auth: opts.auth ?? mockAuth(),
|
|
233
|
+
fieldTypeRegistry: {
|
|
234
|
+
register: () => { },
|
|
235
|
+
get: () => undefined,
|
|
236
|
+
has: () => false,
|
|
237
|
+
list: () => [],
|
|
238
|
+
getAll: () => [],
|
|
239
|
+
deserialize: (_t, v) => v,
|
|
240
|
+
serialize: (_t, v) => v,
|
|
241
|
+
},
|
|
242
|
+
events: events,
|
|
243
|
+
checkPermission: async () => true,
|
|
244
|
+
getUserRoles: async () => ['admin'],
|
|
245
|
+
DDLManager: {},
|
|
246
|
+
services: mockServiceRegistry(),
|
|
247
|
+
queryAlter: mockQueryAlterScope(),
|
|
248
|
+
entityAccess: mockEntityAccessScope(),
|
|
249
|
+
registerPublicRoute: () => { },
|
|
250
|
+
internals: {},
|
|
251
|
+
...(opts.extra ?? {}),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Spin up a Hono test app with the extension's routes registered. Returns
|
|
256
|
+
* the outer Hono so callers can use `.request(path, init)` for assertions.
|
|
257
|
+
*
|
|
258
|
+
* Honors the extension's `mountStrategy`. For `'subapp'`, the outer app
|
|
259
|
+
* mounts the sub-app at `/ext/<extension.name>` by default — same shape
|
|
260
|
+
* the engine produces in production.
|
|
261
|
+
*/
|
|
262
|
+
export async function createTestApp(extension, opts = {}) {
|
|
263
|
+
const ctx = opts.ctx ?? createTestContext();
|
|
264
|
+
const strategy = extension.mountStrategy ?? 'global';
|
|
265
|
+
if (strategy === 'subapp') {
|
|
266
|
+
const outer = new Hono();
|
|
267
|
+
const sub = new Hono();
|
|
268
|
+
await extension.register(sub, ctx);
|
|
269
|
+
const mountPath = opts.mountSubappAt === false ? '/' : (opts.mountSubappAt ?? `/ext/${extension.name}`);
|
|
270
|
+
outer.route(mountPath, sub);
|
|
271
|
+
return outer;
|
|
272
|
+
}
|
|
273
|
+
const app = new Hono();
|
|
274
|
+
await extension.register(app, ctx);
|
|
275
|
+
return app;
|
|
276
|
+
}
|
|
277
|
+
// ─── Integration test helpers (Postgres) ─────────────────────────────────
|
|
278
|
+
//
|
|
279
|
+
// `withTestDb` + friends live in a separate module so the testcontainers
|
|
280
|
+
// dynamic import doesn't fan out on every consumer of `mockDb` /
|
|
281
|
+
// `createTestContext`. Re-exported here for the
|
|
282
|
+
// `import { withTestDb } from '@zveltio/sdk/testing'` ergonomics.
|
|
283
|
+
export { withTestDb, startTestDb, applyMigrationStrings, applyMigrationFiles, stopReusedTestDb, splitStatements, } from './with-test-db.js';
|