@pylonsync/sdk 0.2.4
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/package.json +14 -0
- package/src/index.ts +503 -0
- package/tsconfig.json +7 -0
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pylonsync/sdk",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "0.2.4",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "src/index.ts",
|
|
9
|
+
"types": "src/index.ts",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc -p tsconfig.json --noEmit",
|
|
12
|
+
"check": "tsc -p tsconfig.json --noEmit"
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Route modes
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export type RouteMode = "static" | "server" | "live";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Field types
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export type FieldType =
|
|
12
|
+
| "string"
|
|
13
|
+
| "int"
|
|
14
|
+
| "float"
|
|
15
|
+
| "bool"
|
|
16
|
+
| "datetime"
|
|
17
|
+
| "richtext"
|
|
18
|
+
| `id(${string})`;
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Field builder
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* CRDT container override for a field. Wire format is the kebab-case
|
|
26
|
+
* string each variant maps to (`"text"`, `"counter"`, `"movable-list"`,
|
|
27
|
+
* etc.). Mirror of `pylon_kernel::CrdtAnnotation` on the Rust side.
|
|
28
|
+
*
|
|
29
|
+
* - `"text"` upgrades a `string` to LoroText (collaborative
|
|
30
|
+
* character-level merge instead of LWW).
|
|
31
|
+
* - `"counter"` flips an `int` / `float` to LoroCounter so concurrent
|
|
32
|
+
* increments add instead of stomping each other.
|
|
33
|
+
* - `"list"`, `"movable-list"`, `"tree"` are reserved for ordered /
|
|
34
|
+
* reorderable / hierarchical collections — wire format locked in,
|
|
35
|
+
* server-side projection still pending implementation.
|
|
36
|
+
* - `"lww"` is explicit (matches the default for most scalar types).
|
|
37
|
+
*/
|
|
38
|
+
export type CrdtAnnotation =
|
|
39
|
+
| "lww"
|
|
40
|
+
| "text"
|
|
41
|
+
| "counter"
|
|
42
|
+
| "list"
|
|
43
|
+
| "movable-list"
|
|
44
|
+
| "tree";
|
|
45
|
+
|
|
46
|
+
export interface FieldDefinition {
|
|
47
|
+
type: FieldType;
|
|
48
|
+
optional: boolean;
|
|
49
|
+
unique: boolean;
|
|
50
|
+
/** CRDT container override. Omitted entirely for the default
|
|
51
|
+
* (LWW for scalars, LoroText for richtext). */
|
|
52
|
+
crdt?: CrdtAnnotation;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface FieldBuilder {
|
|
56
|
+
readonly _def: FieldDefinition;
|
|
57
|
+
optional(): FieldBuilder;
|
|
58
|
+
unique(): FieldBuilder;
|
|
59
|
+
/**
|
|
60
|
+
* Override the CRDT container for this field. See [`CrdtAnnotation`]
|
|
61
|
+
* for the full list. Most apps never call this — the default mapping
|
|
62
|
+
* (string→LWW, richtext→LoroText, …) is the right answer.
|
|
63
|
+
*
|
|
64
|
+
* Example: `field.string().crdt("text")` upgrades a string to a
|
|
65
|
+
* collaborative LoroText so two browser tabs editing the field
|
|
66
|
+
* concurrently merge cleanly instead of last-write-wins.
|
|
67
|
+
*/
|
|
68
|
+
crdt(annotation: CrdtAnnotation): FieldBuilder;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createFieldBuilder(type: FieldType): FieldBuilder {
|
|
72
|
+
return buildField({ type, optional: false, unique: false });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildField(def: FieldDefinition): FieldBuilder {
|
|
76
|
+
return {
|
|
77
|
+
_def: def,
|
|
78
|
+
optional() {
|
|
79
|
+
return buildField({ ...def, optional: true });
|
|
80
|
+
},
|
|
81
|
+
unique() {
|
|
82
|
+
return buildField({ ...def, unique: true });
|
|
83
|
+
},
|
|
84
|
+
crdt(annotation) {
|
|
85
|
+
return buildField({ ...def, crdt: annotation });
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Both naming conventions ("bool"/"boolean", "float"/"number") are
|
|
91
|
+
// accepted here to match the validator side (`@pylonsync/functions`,
|
|
92
|
+
// where `v.bool/v.boolean` and `v.float/v.number` are aliases). Keeping
|
|
93
|
+
// both forms alive eliminates a real class of 'module fails to load'
|
|
94
|
+
// bugs caused by guessing which camp the API falls into.
|
|
95
|
+
export const field = {
|
|
96
|
+
string: () => createFieldBuilder("string"),
|
|
97
|
+
int: () => createFieldBuilder("int"),
|
|
98
|
+
float: () => createFieldBuilder("float"),
|
|
99
|
+
/** Alias for `field.float()`. Lets either name work. */
|
|
100
|
+
number: () => createFieldBuilder("float"),
|
|
101
|
+
bool: () => createFieldBuilder("bool"),
|
|
102
|
+
/** Alias for `field.bool()`. Lets either name work. */
|
|
103
|
+
boolean: () => createFieldBuilder("bool"),
|
|
104
|
+
datetime: () => createFieldBuilder("datetime"),
|
|
105
|
+
richtext: () => createFieldBuilder("richtext"),
|
|
106
|
+
id: (target: string) => createFieldBuilder(`id(${target})`),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Entity builder
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
export interface IndexDefinition {
|
|
114
|
+
name: string;
|
|
115
|
+
fields: string[];
|
|
116
|
+
unique: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface RelationDefinition {
|
|
120
|
+
name: string;
|
|
121
|
+
target: string;
|
|
122
|
+
field: string;
|
|
123
|
+
many?: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Per-entity search config. Presence of this object on an entity
|
|
128
|
+
* definition tells Pylon to create FTS5 + facet-bitmap shadow tables
|
|
129
|
+
* on the next schema push and maintain them on every write.
|
|
130
|
+
*
|
|
131
|
+
* - `text` – fields that participate in free-text MATCH (BM25).
|
|
132
|
+
* - `facets` – scalar fields (string / int / bool) that get live
|
|
133
|
+
* per-value counts via `db.useSearch`.
|
|
134
|
+
* - `sortable` – fields the client may order results by. Any `sort`
|
|
135
|
+
* on a field not in this list is silently ignored.
|
|
136
|
+
*/
|
|
137
|
+
export interface SearchConfig {
|
|
138
|
+
text?: string[];
|
|
139
|
+
facets?: string[];
|
|
140
|
+
sortable?: string[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface EntityDefinition {
|
|
144
|
+
name: string;
|
|
145
|
+
fields: Record<string, FieldBuilder>;
|
|
146
|
+
indexes?: IndexDefinition[];
|
|
147
|
+
relations?: RelationDefinition[];
|
|
148
|
+
search?: SearchConfig;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function entity(
|
|
152
|
+
name: string,
|
|
153
|
+
fields: Record<string, FieldBuilder>,
|
|
154
|
+
options?: {
|
|
155
|
+
indexes?: IndexDefinition[];
|
|
156
|
+
relations?: RelationDefinition[];
|
|
157
|
+
search?: SearchConfig;
|
|
158
|
+
},
|
|
159
|
+
): EntityDefinition {
|
|
160
|
+
return {
|
|
161
|
+
name,
|
|
162
|
+
fields,
|
|
163
|
+
indexes: options?.indexes,
|
|
164
|
+
relations: options?.relations,
|
|
165
|
+
search: options?.search,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function relation(def: RelationDefinition): RelationDefinition {
|
|
170
|
+
return def;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Route definition
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
export type AuthMode = "public" | "user";
|
|
178
|
+
|
|
179
|
+
export interface RouteDefinition {
|
|
180
|
+
path: string;
|
|
181
|
+
mode: RouteMode;
|
|
182
|
+
query?: string;
|
|
183
|
+
auth?: AuthMode;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function defineRoute(route: RouteDefinition): RouteDefinition {
|
|
187
|
+
return route;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Query definition
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
export interface InputFieldDefinition {
|
|
195
|
+
name: string;
|
|
196
|
+
type: FieldType;
|
|
197
|
+
optional?: boolean;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface QueryDefinition {
|
|
201
|
+
name: string;
|
|
202
|
+
input?: InputFieldDefinition[];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function query(
|
|
206
|
+
name: string,
|
|
207
|
+
options?: { input?: InputFieldDefinition[] }
|
|
208
|
+
): QueryDefinition {
|
|
209
|
+
return { name, input: options?.input };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Action definition
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
export interface ActionDefinition {
|
|
217
|
+
name: string;
|
|
218
|
+
input?: InputFieldDefinition[];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function action(
|
|
222
|
+
name: string,
|
|
223
|
+
options?: { input?: InputFieldDefinition[] }
|
|
224
|
+
): ActionDefinition {
|
|
225
|
+
return { name, input: options?.input };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Policy definition
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
export interface PolicyDefinition {
|
|
233
|
+
name: string;
|
|
234
|
+
entity?: string;
|
|
235
|
+
action?: string;
|
|
236
|
+
/**
|
|
237
|
+
* Fallback allow expression — evaluated when a more-specific
|
|
238
|
+
* allowRead/allowWrite/allowUpdate/allowDelete isn't set. Kept for
|
|
239
|
+
* backwards compatibility with single-gate policies.
|
|
240
|
+
*/
|
|
241
|
+
allow?: string;
|
|
242
|
+
/** Overrides `allow` for reads (pull, list, get). */
|
|
243
|
+
allowRead?: string;
|
|
244
|
+
/** Overrides `allow` for inserts. Falls back to `allowWrite`. */
|
|
245
|
+
allowInsert?: string;
|
|
246
|
+
/** Overrides `allow`/`allowWrite` for updates. */
|
|
247
|
+
allowUpdate?: string;
|
|
248
|
+
/** Overrides `allow`/`allowWrite` for deletes. */
|
|
249
|
+
allowDelete?: string;
|
|
250
|
+
/** Shared fallback for any write when the specific rule is missing. */
|
|
251
|
+
allowWrite?: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function policy(def: PolicyDefinition): PolicyDefinition {
|
|
255
|
+
return def;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Plugin definition
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
export interface PluginDefinition {
|
|
263
|
+
name: string;
|
|
264
|
+
entities?: EntityDefinition[];
|
|
265
|
+
hooks?: {
|
|
266
|
+
beforeInsert?: (entity: string, data: Record<string, unknown>) => Record<string, unknown> | null;
|
|
267
|
+
afterInsert?: (entity: string, id: string, data: Record<string, unknown>) => void;
|
|
268
|
+
beforeUpdate?: (entity: string, id: string, data: Record<string, unknown>) => Record<string, unknown> | null;
|
|
269
|
+
afterUpdate?: (entity: string, id: string, data: Record<string, unknown>) => void;
|
|
270
|
+
beforeDelete?: (entity: string, id: string) => boolean;
|
|
271
|
+
afterDelete?: (entity: string, id: string) => void;
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function definePlugin(def: PluginDefinition): PluginDefinition {
|
|
276
|
+
return def;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Manifest generation
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
export interface ManifestField {
|
|
284
|
+
name: string;
|
|
285
|
+
type: FieldType;
|
|
286
|
+
optional: boolean;
|
|
287
|
+
unique: boolean;
|
|
288
|
+
/** CRDT container override; matches `pylon_kernel::CrdtAnnotation` on
|
|
289
|
+
* the Rust side. Omitted entirely when the field uses the default. */
|
|
290
|
+
crdt?: CrdtAnnotation;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export interface ManifestIndex {
|
|
294
|
+
name: string;
|
|
295
|
+
fields: string[];
|
|
296
|
+
unique: boolean;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export interface ManifestRelation {
|
|
300
|
+
name: string;
|
|
301
|
+
target: string;
|
|
302
|
+
field: string;
|
|
303
|
+
many?: boolean;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export interface ManifestEntity {
|
|
307
|
+
name: string;
|
|
308
|
+
fields: ManifestField[];
|
|
309
|
+
indexes: ManifestIndex[];
|
|
310
|
+
relations?: ManifestRelation[];
|
|
311
|
+
/**
|
|
312
|
+
* Mirrors `pylon_kernel::ManifestSearchConfig`. When present, the
|
|
313
|
+
* runtime creates FTS5 + facet-bitmap shadow tables on schema push
|
|
314
|
+
* and maintains them on every write.
|
|
315
|
+
*/
|
|
316
|
+
search?: {
|
|
317
|
+
text?: string[];
|
|
318
|
+
facets?: string[];
|
|
319
|
+
sortable?: string[];
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export interface ManifestRoute {
|
|
324
|
+
path: string;
|
|
325
|
+
mode: string;
|
|
326
|
+
query?: string;
|
|
327
|
+
auth?: string;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export interface ManifestInputField {
|
|
331
|
+
name: string;
|
|
332
|
+
type: FieldType;
|
|
333
|
+
optional: boolean;
|
|
334
|
+
unique: false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export interface ManifestQuery {
|
|
338
|
+
name: string;
|
|
339
|
+
input?: ManifestInputField[];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export interface ManifestAction {
|
|
343
|
+
name: string;
|
|
344
|
+
input?: ManifestInputField[];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export interface ManifestPolicy {
|
|
348
|
+
name: string;
|
|
349
|
+
entity?: string;
|
|
350
|
+
action?: string;
|
|
351
|
+
allow?: string;
|
|
352
|
+
allowRead?: string;
|
|
353
|
+
allowInsert?: string;
|
|
354
|
+
allowUpdate?: string;
|
|
355
|
+
allowDelete?: string;
|
|
356
|
+
allowWrite?: string;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export const MANIFEST_VERSION = 1;
|
|
360
|
+
|
|
361
|
+
export interface AppManifest {
|
|
362
|
+
manifest_version: number;
|
|
363
|
+
name: string;
|
|
364
|
+
version: string;
|
|
365
|
+
entities: ManifestEntity[];
|
|
366
|
+
routes: ManifestRoute[];
|
|
367
|
+
queries: ManifestQuery[];
|
|
368
|
+
actions: ManifestAction[];
|
|
369
|
+
policies: ManifestPolicy[];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function entitiesToManifest(
|
|
373
|
+
entities: EntityDefinition[]
|
|
374
|
+
): ManifestEntity[] {
|
|
375
|
+
return entities.map((e) => {
|
|
376
|
+
const result: ManifestEntity = {
|
|
377
|
+
name: e.name,
|
|
378
|
+
fields: Object.entries(e.fields).map(([name, fb]) => {
|
|
379
|
+
const f: ManifestField = {
|
|
380
|
+
name,
|
|
381
|
+
type: fb._def.type,
|
|
382
|
+
optional: fb._def.optional,
|
|
383
|
+
unique: fb._def.unique,
|
|
384
|
+
};
|
|
385
|
+
// Emit `crdt` only when set — keeps default-shape manifests
|
|
386
|
+
// visually identical to pre-CRDT versions in JSON diffs.
|
|
387
|
+
if (fb._def.crdt !== undefined) {
|
|
388
|
+
f.crdt = fb._def.crdt;
|
|
389
|
+
}
|
|
390
|
+
return f;
|
|
391
|
+
}),
|
|
392
|
+
indexes: (e.indexes ?? []).map((idx) => ({
|
|
393
|
+
name: idx.name,
|
|
394
|
+
fields: idx.fields,
|
|
395
|
+
unique: idx.unique,
|
|
396
|
+
})),
|
|
397
|
+
};
|
|
398
|
+
if (e.relations && e.relations.length > 0) {
|
|
399
|
+
result.relations = e.relations.map((r) => ({
|
|
400
|
+
name: r.name,
|
|
401
|
+
target: r.target,
|
|
402
|
+
field: r.field,
|
|
403
|
+
many: r.many,
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
406
|
+
if (e.search) {
|
|
407
|
+
const s = e.search;
|
|
408
|
+
// Only emit the block when at least one list is non-empty — keeps
|
|
409
|
+
// the manifest JSON clean for non-searchable entities.
|
|
410
|
+
const anyDeclared =
|
|
411
|
+
(s.text?.length ?? 0) > 0 ||
|
|
412
|
+
(s.facets?.length ?? 0) > 0 ||
|
|
413
|
+
(s.sortable?.length ?? 0) > 0;
|
|
414
|
+
if (anyDeclared) {
|
|
415
|
+
result.search = {
|
|
416
|
+
text: s.text ?? [],
|
|
417
|
+
facets: s.facets ?? [],
|
|
418
|
+
sortable: s.sortable ?? [],
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return result;
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function routesToManifest(routes: RouteDefinition[]): ManifestRoute[] {
|
|
427
|
+
return routes.map((r) => {
|
|
428
|
+
const result: ManifestRoute = { path: r.path, mode: r.mode };
|
|
429
|
+
if (r.query) result.query = r.query;
|
|
430
|
+
if (r.auth) result.auth = r.auth;
|
|
431
|
+
return result;
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function queriesToManifest(queries: QueryDefinition[]): ManifestQuery[] {
|
|
436
|
+
return queries.map((q) => {
|
|
437
|
+
const result: ManifestQuery = { name: q.name };
|
|
438
|
+
if (q.input && q.input.length > 0) {
|
|
439
|
+
result.input = q.input.map((f) => ({
|
|
440
|
+
name: f.name,
|
|
441
|
+
type: f.type,
|
|
442
|
+
optional: f.optional ?? false,
|
|
443
|
+
unique: false as const,
|
|
444
|
+
}));
|
|
445
|
+
}
|
|
446
|
+
return result;
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function actionsToManifest(
|
|
451
|
+
actions: ActionDefinition[]
|
|
452
|
+
): ManifestAction[] {
|
|
453
|
+
return actions.map((a) => {
|
|
454
|
+
const result: ManifestAction = { name: a.name };
|
|
455
|
+
if (a.input && a.input.length > 0) {
|
|
456
|
+
result.input = a.input.map((f) => ({
|
|
457
|
+
name: f.name,
|
|
458
|
+
type: f.type,
|
|
459
|
+
optional: f.optional ?? false,
|
|
460
|
+
unique: false as const,
|
|
461
|
+
}));
|
|
462
|
+
}
|
|
463
|
+
return result;
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function policiesToManifest(
|
|
468
|
+
policies: PolicyDefinition[]
|
|
469
|
+
): ManifestPolicy[] {
|
|
470
|
+
return policies.map((p) => {
|
|
471
|
+
const result: ManifestPolicy = { name: p.name };
|
|
472
|
+
if (p.allow) result.allow = p.allow;
|
|
473
|
+
if (p.allowRead) result.allowRead = p.allowRead;
|
|
474
|
+
if (p.allowInsert) result.allowInsert = p.allowInsert;
|
|
475
|
+
if (p.allowUpdate) result.allowUpdate = p.allowUpdate;
|
|
476
|
+
if (p.allowDelete) result.allowDelete = p.allowDelete;
|
|
477
|
+
if (p.allowWrite) result.allowWrite = p.allowWrite;
|
|
478
|
+
if (p.entity) result.entity = p.entity;
|
|
479
|
+
if (p.action) result.action = p.action;
|
|
480
|
+
return result;
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function buildManifest(options: {
|
|
485
|
+
name: string;
|
|
486
|
+
version: string;
|
|
487
|
+
entities: EntityDefinition[];
|
|
488
|
+
routes: RouteDefinition[];
|
|
489
|
+
queries?: QueryDefinition[];
|
|
490
|
+
actions?: ActionDefinition[];
|
|
491
|
+
policies?: PolicyDefinition[];
|
|
492
|
+
}): AppManifest {
|
|
493
|
+
return {
|
|
494
|
+
manifest_version: MANIFEST_VERSION,
|
|
495
|
+
name: options.name,
|
|
496
|
+
version: options.version,
|
|
497
|
+
entities: entitiesToManifest(options.entities),
|
|
498
|
+
routes: routesToManifest(options.routes),
|
|
499
|
+
queries: queriesToManifest(options.queries ?? []),
|
|
500
|
+
actions: actionsToManifest(options.actions ?? []),
|
|
501
|
+
policies: policiesToManifest(options.policies ?? []),
|
|
502
|
+
};
|
|
503
|
+
}
|