@vertz/server 0.2.16 → 0.2.18
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/index.d.ts +176 -135
- package/dist/index.js +406 -200
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,46 @@ import { ModelEntry } from "@vertz/db";
|
|
|
4
4
|
import { AuthError, Result } from "@vertz/errors";
|
|
5
5
|
import { Infer, ObjectSchema, StringSchema } from "@vertz/schema";
|
|
6
6
|
/**
|
|
7
|
+
* InMemoryClosureStore — closure table for resource hierarchy.
|
|
8
|
+
*
|
|
9
|
+
* Stores ancestor/descendant relationships with depth tracking.
|
|
10
|
+
* Self-reference rows (depth 0) are created for every resource.
|
|
11
|
+
* Hierarchy depth is capped at 4 levels.
|
|
12
|
+
*/
|
|
13
|
+
interface ClosureRow {
|
|
14
|
+
ancestorType: string;
|
|
15
|
+
ancestorId: string;
|
|
16
|
+
descendantType: string;
|
|
17
|
+
descendantId: string;
|
|
18
|
+
depth: number;
|
|
19
|
+
}
|
|
20
|
+
interface ClosureEntry {
|
|
21
|
+
type: string;
|
|
22
|
+
id: string;
|
|
23
|
+
depth: number;
|
|
24
|
+
}
|
|
25
|
+
interface ParentRef {
|
|
26
|
+
parentType: string;
|
|
27
|
+
parentId: string;
|
|
28
|
+
}
|
|
29
|
+
interface ClosureStore {
|
|
30
|
+
addResource(type: string, id: string, parent?: ParentRef): Promise<void>;
|
|
31
|
+
removeResource(type: string, id: string): Promise<void>;
|
|
32
|
+
getAncestors(type: string, id: string): Promise<ClosureEntry[]>;
|
|
33
|
+
getDescendants(type: string, id: string): Promise<ClosureEntry[]>;
|
|
34
|
+
hasPath(ancestorType: string, ancestorId: string, descendantType: string, descendantId: string): Promise<boolean>;
|
|
35
|
+
dispose(): void;
|
|
36
|
+
}
|
|
37
|
+
declare class InMemoryClosureStore implements ClosureStore {
|
|
38
|
+
private rows;
|
|
39
|
+
addResource(type: string, id: string, parent?: ParentRef): Promise<void>;
|
|
40
|
+
removeResource(type: string, id: string): Promise<void>;
|
|
41
|
+
getAncestors(type: string, id: string): Promise<ClosureEntry[]>;
|
|
42
|
+
getDescendants(type: string, id: string): Promise<ClosureEntry[]>;
|
|
43
|
+
hasPath(ancestorType: string, ancestorId: string, descendantType: string, descendantId: string): Promise<boolean>;
|
|
44
|
+
dispose(): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
7
47
|
* rules.* builders — declarative access rule data structures.
|
|
8
48
|
*
|
|
9
49
|
* These are pure data structures with no evaluation logic.
|
|
@@ -207,43 +247,102 @@ interface AccessDefinition {
|
|
|
207
247
|
}
|
|
208
248
|
declare function defineAccess(input: DefineAccessInput): AccessDefinition;
|
|
209
249
|
/**
|
|
210
|
-
*
|
|
250
|
+
* Feature Flag Store — per-tenant boolean feature flags.
|
|
211
251
|
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* Hierarchy depth is capped at 4 levels.
|
|
252
|
+
* Pluggable interface with in-memory default.
|
|
253
|
+
* Used by Layer 1 of access context to gate entitlements on feature flags.
|
|
215
254
|
*/
|
|
216
|
-
interface
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
descendantId: string;
|
|
221
|
-
depth: number;
|
|
255
|
+
interface FlagStore {
|
|
256
|
+
setFlag(tenantId: string, flag: string, enabled: boolean): void;
|
|
257
|
+
getFlag(tenantId: string, flag: string): boolean;
|
|
258
|
+
getFlags(tenantId: string): Record<string, boolean>;
|
|
222
259
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
260
|
+
declare class InMemoryFlagStore implements FlagStore {
|
|
261
|
+
private flags;
|
|
262
|
+
setFlag(tenantId: string, flag: string, enabled: boolean): void;
|
|
263
|
+
getFlag(tenantId: string, flag: string): boolean;
|
|
264
|
+
getFlags(tenantId: string): Record<string, boolean>;
|
|
227
265
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
266
|
+
/** Limit override: add (additive) or max (hard cap) */
|
|
267
|
+
interface LimitOverrideDef {
|
|
268
|
+
add?: number;
|
|
269
|
+
max?: number;
|
|
231
270
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
271
|
+
/** Per-tenant overrides */
|
|
272
|
+
interface TenantOverrides {
|
|
273
|
+
features?: string[];
|
|
274
|
+
limits?: Record<string, LimitOverrideDef>;
|
|
275
|
+
}
|
|
276
|
+
interface OverrideStore {
|
|
277
|
+
set(tenantId: string, overrides: TenantOverrides): Promise<void>;
|
|
278
|
+
remove(tenantId: string, keys: {
|
|
279
|
+
features?: string[];
|
|
280
|
+
limits?: string[];
|
|
281
|
+
}): Promise<void>;
|
|
282
|
+
get(tenantId: string): Promise<TenantOverrides | null>;
|
|
238
283
|
dispose(): void;
|
|
239
284
|
}
|
|
240
|
-
declare class
|
|
241
|
-
private
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
285
|
+
declare class InMemoryOverrideStore implements OverrideStore {
|
|
286
|
+
private overrides;
|
|
287
|
+
set(tenantId: string, overrides: TenantOverrides): Promise<void>;
|
|
288
|
+
remove(tenantId: string, keys: {
|
|
289
|
+
features?: string[];
|
|
290
|
+
limits?: string[];
|
|
291
|
+
}): Promise<void>;
|
|
292
|
+
get(tenantId: string): Promise<TenantOverrides | null>;
|
|
293
|
+
dispose(): void;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Validate tenant overrides against the access definition.
|
|
297
|
+
* Throws on invalid limit keys, invalid feature names, or invalid max values.
|
|
298
|
+
*/
|
|
299
|
+
declare function validateOverrides(accessDef: AccessDefinition, overrides: TenantOverrides): void;
|
|
300
|
+
/**
|
|
301
|
+
* Plan Version Store — tracks versioned snapshots of plan configurations.
|
|
302
|
+
*
|
|
303
|
+
* When a plan's config changes (hash differs), a new version is created
|
|
304
|
+
* with a snapshot of the plan's features, limits, and price at that point.
|
|
305
|
+
*/
|
|
306
|
+
interface PlanSnapshot {
|
|
307
|
+
features: readonly string[] | string[];
|
|
308
|
+
limits: Record<string, unknown>;
|
|
309
|
+
price: {
|
|
310
|
+
amount: number;
|
|
311
|
+
interval: string;
|
|
312
|
+
} | null;
|
|
313
|
+
}
|
|
314
|
+
interface PlanVersionInfo {
|
|
315
|
+
planId: string;
|
|
316
|
+
version: number;
|
|
317
|
+
hash: string;
|
|
318
|
+
snapshot: PlanSnapshot;
|
|
319
|
+
createdAt: Date;
|
|
320
|
+
}
|
|
321
|
+
interface PlanVersionStore {
|
|
322
|
+
/** Create a new version for a plan. Returns the version number. */
|
|
323
|
+
createVersion(planId: string, hash: string, snapshot: PlanSnapshot): Promise<number>;
|
|
324
|
+
/** Get the current (latest) version number for a plan. Returns null if no versions exist. */
|
|
325
|
+
getCurrentVersion(planId: string): Promise<number | null>;
|
|
326
|
+
/** Get a specific version's info. Returns null if not found. */
|
|
327
|
+
getVersion(planId: string, version: number): Promise<PlanVersionInfo | null>;
|
|
328
|
+
/** Get the version number a tenant is on for a given plan. Returns null if not set. */
|
|
329
|
+
getTenantVersion(tenantId: string, planId: string): Promise<number | null>;
|
|
330
|
+
/** Set the version number a tenant is on for a given plan. */
|
|
331
|
+
setTenantVersion(tenantId: string, planId: string, version: number): Promise<void>;
|
|
332
|
+
/** Get the hash of the current (latest) version for a plan. Returns null if no versions. */
|
|
333
|
+
getCurrentHash(planId: string): Promise<string | null>;
|
|
334
|
+
/** Clean up resources. */
|
|
335
|
+
dispose(): void;
|
|
336
|
+
}
|
|
337
|
+
declare class InMemoryPlanVersionStore implements PlanVersionStore {
|
|
338
|
+
private versions;
|
|
339
|
+
private tenantVersions;
|
|
340
|
+
createVersion(planId: string, hash: string, snapshot: PlanSnapshot): Promise<number>;
|
|
341
|
+
getCurrentVersion(planId: string): Promise<number | null>;
|
|
342
|
+
getVersion(planId: string, version: number): Promise<PlanVersionInfo | null>;
|
|
343
|
+
getTenantVersion(tenantId: string, planId: string): Promise<number | null>;
|
|
344
|
+
setTenantVersion(tenantId: string, planId: string, version: number): Promise<void>;
|
|
345
|
+
getCurrentHash(planId: string): Promise<string | null>;
|
|
247
346
|
dispose(): void;
|
|
248
347
|
}
|
|
249
348
|
interface RoleAssignment {
|
|
@@ -274,23 +373,6 @@ declare class InMemoryRoleAssignmentStore implements RoleAssignmentStore {
|
|
|
274
373
|
getEffectiveRole(userId: string, resourceType: string, resourceId: string, accessDef: AccessDefinition, closureStore: ClosureStore): Promise<string | null>;
|
|
275
374
|
dispose(): void;
|
|
276
375
|
}
|
|
277
|
-
/**
|
|
278
|
-
* Feature Flag Store — per-tenant boolean feature flags.
|
|
279
|
-
*
|
|
280
|
-
* Pluggable interface with in-memory default.
|
|
281
|
-
* Used by Layer 1 of access context to gate entitlements on feature flags.
|
|
282
|
-
*/
|
|
283
|
-
interface FlagStore {
|
|
284
|
-
setFlag(tenantId: string, flag: string, enabled: boolean): void;
|
|
285
|
-
getFlag(tenantId: string, flag: string): boolean;
|
|
286
|
-
getFlags(tenantId: string): Record<string, boolean>;
|
|
287
|
-
}
|
|
288
|
-
declare class InMemoryFlagStore implements FlagStore {
|
|
289
|
-
private flags;
|
|
290
|
-
setFlag(tenantId: string, flag: string, enabled: boolean): void;
|
|
291
|
-
getFlag(tenantId: string, flag: string): boolean;
|
|
292
|
-
getFlags(tenantId: string): Record<string, boolean>;
|
|
293
|
-
}
|
|
294
376
|
/** Per-tenant limit override. Only affects the cap, not the billing period. */
|
|
295
377
|
interface LimitOverride {
|
|
296
378
|
max: number;
|
|
@@ -377,88 +459,6 @@ declare class InMemoryWalletStore implements WalletStore {
|
|
|
377
459
|
getConsumption(tenantId: string, entitlement: string, periodStart: Date, _periodEnd: Date): Promise<number>;
|
|
378
460
|
dispose(): void;
|
|
379
461
|
}
|
|
380
|
-
/** Limit override: add (additive) or max (hard cap) */
|
|
381
|
-
interface LimitOverrideDef {
|
|
382
|
-
add?: number;
|
|
383
|
-
max?: number;
|
|
384
|
-
}
|
|
385
|
-
/** Per-tenant overrides */
|
|
386
|
-
interface TenantOverrides {
|
|
387
|
-
features?: string[];
|
|
388
|
-
limits?: Record<string, LimitOverrideDef>;
|
|
389
|
-
}
|
|
390
|
-
interface OverrideStore {
|
|
391
|
-
set(tenantId: string, overrides: TenantOverrides): Promise<void>;
|
|
392
|
-
remove(tenantId: string, keys: {
|
|
393
|
-
features?: string[];
|
|
394
|
-
limits?: string[];
|
|
395
|
-
}): Promise<void>;
|
|
396
|
-
get(tenantId: string): Promise<TenantOverrides | null>;
|
|
397
|
-
dispose(): void;
|
|
398
|
-
}
|
|
399
|
-
declare class InMemoryOverrideStore implements OverrideStore {
|
|
400
|
-
private overrides;
|
|
401
|
-
set(tenantId: string, overrides: TenantOverrides): Promise<void>;
|
|
402
|
-
remove(tenantId: string, keys: {
|
|
403
|
-
features?: string[];
|
|
404
|
-
limits?: string[];
|
|
405
|
-
}): Promise<void>;
|
|
406
|
-
get(tenantId: string): Promise<TenantOverrides | null>;
|
|
407
|
-
dispose(): void;
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Validate tenant overrides against the access definition.
|
|
411
|
-
* Throws on invalid limit keys, invalid feature names, or invalid max values.
|
|
412
|
-
*/
|
|
413
|
-
declare function validateOverrides(accessDef: AccessDefinition, overrides: TenantOverrides): void;
|
|
414
|
-
/**
|
|
415
|
-
* Plan Version Store — tracks versioned snapshots of plan configurations.
|
|
416
|
-
*
|
|
417
|
-
* When a plan's config changes (hash differs), a new version is created
|
|
418
|
-
* with a snapshot of the plan's features, limits, and price at that point.
|
|
419
|
-
*/
|
|
420
|
-
interface PlanSnapshot {
|
|
421
|
-
features: readonly string[] | string[];
|
|
422
|
-
limits: Record<string, unknown>;
|
|
423
|
-
price: {
|
|
424
|
-
amount: number;
|
|
425
|
-
interval: string;
|
|
426
|
-
} | null;
|
|
427
|
-
}
|
|
428
|
-
interface PlanVersionInfo {
|
|
429
|
-
planId: string;
|
|
430
|
-
version: number;
|
|
431
|
-
hash: string;
|
|
432
|
-
snapshot: PlanSnapshot;
|
|
433
|
-
createdAt: Date;
|
|
434
|
-
}
|
|
435
|
-
interface PlanVersionStore {
|
|
436
|
-
/** Create a new version for a plan. Returns the version number. */
|
|
437
|
-
createVersion(planId: string, hash: string, snapshot: PlanSnapshot): Promise<number>;
|
|
438
|
-
/** Get the current (latest) version number for a plan. Returns null if no versions exist. */
|
|
439
|
-
getCurrentVersion(planId: string): Promise<number | null>;
|
|
440
|
-
/** Get a specific version's info. Returns null if not found. */
|
|
441
|
-
getVersion(planId: string, version: number): Promise<PlanVersionInfo | null>;
|
|
442
|
-
/** Get the version number a tenant is on for a given plan. Returns null if not set. */
|
|
443
|
-
getTenantVersion(tenantId: string, planId: string): Promise<number | null>;
|
|
444
|
-
/** Set the version number a tenant is on for a given plan. */
|
|
445
|
-
setTenantVersion(tenantId: string, planId: string, version: number): Promise<void>;
|
|
446
|
-
/** Get the hash of the current (latest) version for a plan. Returns null if no versions. */
|
|
447
|
-
getCurrentHash(planId: string): Promise<string | null>;
|
|
448
|
-
/** Clean up resources. */
|
|
449
|
-
dispose(): void;
|
|
450
|
-
}
|
|
451
|
-
declare class InMemoryPlanVersionStore implements PlanVersionStore {
|
|
452
|
-
private versions;
|
|
453
|
-
private tenantVersions;
|
|
454
|
-
createVersion(planId: string, hash: string, snapshot: PlanSnapshot): Promise<number>;
|
|
455
|
-
getCurrentVersion(planId: string): Promise<number | null>;
|
|
456
|
-
getVersion(planId: string, version: number): Promise<PlanVersionInfo | null>;
|
|
457
|
-
getTenantVersion(tenantId: string, planId: string): Promise<number | null>;
|
|
458
|
-
setTenantVersion(tenantId: string, planId: string, version: number): Promise<void>;
|
|
459
|
-
getCurrentHash(planId: string): Promise<string | null>;
|
|
460
|
-
dispose(): void;
|
|
461
|
-
}
|
|
462
462
|
interface ResourceRef {
|
|
463
463
|
type: string;
|
|
464
464
|
id: string;
|
|
@@ -963,11 +963,15 @@ interface AuthInstance {
|
|
|
963
963
|
*/
|
|
964
964
|
resolveSessionForSSR: (request: Request) => Promise<{
|
|
965
965
|
session: {
|
|
966
|
-
user:
|
|
966
|
+
user: {
|
|
967
|
+
id: string;
|
|
968
|
+
email: string;
|
|
969
|
+
role: string;
|
|
970
|
+
[key: string]: unknown;
|
|
971
|
+
};
|
|
967
972
|
expiresAt: number;
|
|
968
973
|
};
|
|
969
|
-
|
|
970
|
-
accessSet?: unknown;
|
|
974
|
+
accessSet?: AccessSet | null;
|
|
971
975
|
} | null>;
|
|
972
976
|
}
|
|
973
977
|
interface AuthContext {
|
|
@@ -1809,6 +1813,43 @@ interface RelationConfigObject<TColumnKeys extends string = string> {
|
|
|
1809
1813
|
readonly maxLimit?: number;
|
|
1810
1814
|
}
|
|
1811
1815
|
type EntityRelationsConfig<TRelations extends Record<string, RelationDef2> = Record<string, RelationDef2>> = { [K in keyof TRelations]? : true | false | RelationConfigObject<RelationColumnKeys<TRelations[K]>> };
|
|
1816
|
+
/** Extract the target table type from a RelationDef. */
|
|
1817
|
+
type RelationTargetTable<R> = R extends RelationDef2<infer T> ? T : TableDef;
|
|
1818
|
+
/** Structured expose config for a relation. Controls field, filter, sort, and nested relation exposure. */
|
|
1819
|
+
interface RelationExposeConfig<R extends RelationDef2 = RelationDef2> {
|
|
1820
|
+
/** Which fields of the related entity to expose. */
|
|
1821
|
+
readonly select: { [K in PublicColumnKeys<RelationTargetTable<R>>]? : true | AccessRule };
|
|
1822
|
+
/** Which fields clients can filter on via VertzQL `where`. */
|
|
1823
|
+
readonly allowWhere?: { [K in PublicColumnKeys<RelationTargetTable<R>>]? : true | AccessRule };
|
|
1824
|
+
/** Which fields clients can sort on via VertzQL `orderBy`. */
|
|
1825
|
+
readonly allowOrderBy?: { [K in PublicColumnKeys<RelationTargetTable<R>>]? : true | AccessRule };
|
|
1826
|
+
/** Max items returned per parent row. Defaults to DEFAULT_RELATION_LIMIT. */
|
|
1827
|
+
readonly maxLimit?: number;
|
|
1828
|
+
/** Nested relation exposure (loosely typed — target model relations not available from RelationDef). */
|
|
1829
|
+
readonly include?: Record<string, true | false | RelationExposeConfig>;
|
|
1830
|
+
}
|
|
1831
|
+
/** Controls which fields, filters, sorts, and relations are exposed through the entity API. */
|
|
1832
|
+
interface ExposeConfig<
|
|
1833
|
+
TTable extends TableDef = TableDef,
|
|
1834
|
+
TModel extends ModelDef4 = ModelDef4
|
|
1835
|
+
> {
|
|
1836
|
+
/** Which fields of the entity to expose. Required when expose is present. */
|
|
1837
|
+
readonly select: { [K in PublicColumnKeys<TTable>]? : true | AccessRule };
|
|
1838
|
+
/** Which fields clients can filter on via VertzQL `where`. */
|
|
1839
|
+
readonly allowWhere?: { [K in PublicColumnKeys<TTable>]? : true | AccessRule };
|
|
1840
|
+
/** Which fields clients can sort on via VertzQL `orderBy`. */
|
|
1841
|
+
readonly allowOrderBy?: { [K in PublicColumnKeys<TTable>]? : true | AccessRule };
|
|
1842
|
+
/** Which relations to expose and how. */
|
|
1843
|
+
readonly include?: { [K in keyof TModel["relations"]]? : true | false | RelationExposeConfig<TModel["relations"][K] extends RelationDef2 ? TModel["relations"][K] : RelationDef2> };
|
|
1844
|
+
}
|
|
1845
|
+
/** Extract non-hidden column keys from a table (public fields). */
|
|
1846
|
+
type PublicColumnKeys<TTable extends TableDef> = TTable extends TableDef<infer TCols> ? { [K in keyof TCols & string] : TCols[K] extends {
|
|
1847
|
+
_meta: {
|
|
1848
|
+
_annotations: {
|
|
1849
|
+
hidden: true;
|
|
1850
|
+
};
|
|
1851
|
+
};
|
|
1852
|
+
} ? never : K }[keyof TCols & string] : string;
|
|
1812
1853
|
interface EntityConfig<
|
|
1813
1854
|
TModel extends ModelDef4 = ModelDef4,
|
|
1814
1855
|
TActions extends Record<string, EntityActionDef<any, any, any, any>> = {},
|
|
@@ -1831,7 +1872,7 @@ interface EntityConfig<
|
|
|
1831
1872
|
readonly delete?: (row: TModel["table"]["$response"], ctx: EntityContext<TModel, TInject>) => void | Promise<void>;
|
|
1832
1873
|
};
|
|
1833
1874
|
readonly actions?: { readonly [K in keyof TActions] : TActions[K] };
|
|
1834
|
-
readonly
|
|
1875
|
+
readonly expose?: ExposeConfig<TModel["table"], TModel>;
|
|
1835
1876
|
}
|
|
1836
1877
|
interface EntityDefinition<TModel extends ModelDef4 = ModelDef4> {
|
|
1837
1878
|
readonly kind: "entity";
|
|
@@ -1842,7 +1883,7 @@ interface EntityDefinition<TModel extends ModelDef4 = ModelDef4> {
|
|
|
1842
1883
|
readonly before: EntityBeforeHooks;
|
|
1843
1884
|
readonly after: EntityAfterHooks;
|
|
1844
1885
|
readonly actions: Record<string, EntityActionDef>;
|
|
1845
|
-
readonly
|
|
1886
|
+
readonly expose?: ExposeConfig<TModel["table"], TModel>;
|
|
1846
1887
|
/** DB table name (defaults to entity name). */
|
|
1847
1888
|
readonly table: string;
|
|
1848
1889
|
/** Whether CRUD auto-filters by tenantId. */
|
|
@@ -2042,4 +2083,4 @@ declare function service<
|
|
|
2042
2083
|
TInject extends Record<string, EntityDefinition> = {},
|
|
2043
2084
|
TActions extends Record<string, ServiceActionDef<any, any, any>> = Record<string, ServiceActionDef<any, any, any>>
|
|
2044
2085
|
>(name: string, config: ServiceConfig<TActions, TInject>): ServiceDefinition;
|
|
2045
|
-
export { vertz, verifyPassword, validatePassword, validateOverrides, validateAuthModels, stripReadOnlyFields, stripHiddenFields, service, rules, resolveTenantChain, makeImmutable, isContentDescriptor, initializeAuthTables, hashPassword, google, github, getIncompatibleAddOns, generateEntityRoutes, entityErrorHandler, entity, enforceAccess, encodeAccessSet, discord, defineAccess, defaultAccess, deepFreeze, decodeAccessSet, createWebhookHandler, createStripeBillingAdapter, createServer, createPlanManager, createMiddleware, createImmutableProxy, createEnv, createEntityContext, createCrudHandlers, createBillingEventEmitter, createAuth, createAccessEventBroadcaster, createAccessContext, createAccess, content, computePlanHash, computeOverage, computeEntityAccess, computeAccessSet, checkFva, checkAddOnCompatibility, calculateBillingPeriod, authModels, WebhookHandlerConfig, WalletStore, WalletEntry, VertzException, ValidationException, UserTableEntry, UserStore, UnauthorizedException, TenantOverrides, TenantChainHop, TenantChain, SubscriptionStore, Subscription, StripeProduct, StripePrice, StripeClient, StripeBillingAdapterConfig, StoredSession, StoredPasswordReset, StoredEmailVerification, SignUpInput, SignInInput, SessionStrategy, SessionStore, SessionPayload, SessionInfo, SessionConfig, Session, ServiceUnavailableException, ServiceRequestInfo, ServiceDefinition, ServiceContext, ServiceConfig, ServiceActionDef, ServerInstance, ServerHandle, ServerConfig, ServerAdapter, RuleContext, RoleAssignmentTableEntry, RoleAssignmentStore, RoleAssignment, ResourceRef, Resource, RequestInfo, RawRequest, RateLimitStore, RateLimitResult, RateLimitConfig, PriceInterval, PlanVersionStore, PlanVersionInfo, PlanSnapshot, PlanPrice, PlanHashInput, PlanDef, Period, PasswordResetStore, PasswordResetConfig, PasswordRequirements, ParentRef, OverrideStore, OverageInput, OverageConfig, OnUserCreatedPayload, OAuthUserInfo, OAuthTokens, OAuthProviderConfig, OAuthProvider, OAuthAccountStore, NotFoundException, NamedMiddlewareDef, MiddlewareDef, MfaSetupData, MfaConfig, MfaChallengeData, MFAStore, ListenOptions, ListResult, ListOptions2 as ListOptions, LimitOverrideDef, LimitOverride, LimitDef, InternalServerErrorException, InferSchema, Infer2 as Infer, InMemoryWalletStore, InMemoryUserStore, InMemorySubscriptionStore, InMemorySessionStore, InMemoryRoleAssignmentStore, InMemoryRateLimitStore, InMemoryPlanVersionStore, InMemoryPasswordResetStore, InMemoryOverrideStore, InMemoryOAuthAccountStore, InMemoryMFAStore, InMemoryGrandfatheringStore, InMemoryFlagStore, InMemoryEmailVerificationStore, InMemoryClosureStore, HttpStatusCode, HttpMethod, HandlerCtx, GrandfatheringStore, GrandfatheringState, ForbiddenException, FlagStore, EnvConfig, EntityRouteOptions, EntityRelationsConfig, EntityRegistry, EntityOperations, EntityErrorResult, EntityDefinition, EntityDef, EntityDbAdapter2 as EntityDbAdapter, EntityContext, EntityConfig, EntityActionDef, EntitlementValue, EntitlementRegistry, EntitlementDefinition, EntitlementDef, Entitlement, EncodedAccessSet, EmailVerificationStore, EmailVerificationConfig, EmailPasswordConfig, Deps, DenialReason, DenialMeta, DefineAccessInput, DeepReadonly, DbUserStore, DbSubscriptionStore, DbSessionStore, DbRoleAssignmentStore, DbOAuthAccountStore, DbFlagStore, DbDialectName, DbClosureStore, Ctx, CrudResult, CrudHandlers, CorsConfig, CookieConfig, ContentDescriptor, ConsumeResult, ConflictException, ComputeAccessSetConfig, ClosureStore, ClosureRow, ClosureEntry, BillingPeriod, BillingEventType, BillingEventHandler, BillingEventEmitter, BillingEvent, BillingAdapter, BaseContext, BadRequestException, AuthorizationError, AuthUser, AuthTokens, AuthInstance, AuthEntityProxy, AuthDbClient, AuthContext, AuthConfig, AuthCallbackContext, AuthApi, AppConfig2 as AppConfig, AppBuilder2 as AppBuilder, AddOnRequires, AclClaim, AccumulateProvides, AccessWsData, AccessSet, AccessRule2 as AccessRule, AccessInstance, AccessEventBroadcasterConfig, AccessEventBroadcaster, AccessEvent, AccessDefinition, AccessContextConfig, AccessContext, AccessConfig, AccessCheckResult, AccessCheckData, AUTH_TABLE_NAMES };
|
|
2086
|
+
export { vertz, verifyPassword, validatePassword, validateOverrides, validateAuthModels, stripReadOnlyFields, stripHiddenFields, service, rules, resolveTenantChain, makeImmutable, isContentDescriptor, initializeAuthTables, hashPassword, google, github, getIncompatibleAddOns, generateEntityRoutes, entityErrorHandler, entity, enforceAccess, encodeAccessSet, discord, defineAccess, defaultAccess, deepFreeze, decodeAccessSet, createWebhookHandler, createStripeBillingAdapter, createServer, createPlanManager, createMiddleware, createImmutableProxy, createEnv, createEntityContext, createCrudHandlers, createBillingEventEmitter, createAuth, createAccessEventBroadcaster, createAccessContext, createAccess, content, computePlanHash, computeOverage, computeEntityAccess, computeAccessSet, checkFva, checkAddOnCompatibility, calculateBillingPeriod, authModels, WebhookHandlerConfig, WalletStore, WalletEntry, VertzException, ValidationException, UserTableEntry, UserStore, UnauthorizedException, TenantOverrides, TenantChainHop, TenantChain, SubscriptionStore, Subscription, StripeProduct, StripePrice, StripeClient, StripeBillingAdapterConfig, StoredSession, StoredPasswordReset, StoredEmailVerification, SignUpInput, SignInInput, SessionStrategy, SessionStore, SessionPayload, SessionInfo, SessionConfig, Session, ServiceUnavailableException, ServiceRequestInfo, ServiceDefinition, ServiceContext, ServiceConfig, ServiceActionDef, ServerInstance, ServerHandle, ServerConfig, ServerAdapter, RuleContext, RoleAssignmentTableEntry, RoleAssignmentStore, RoleAssignment, ResourceRef, Resource, RequestInfo, RelationExposeConfig, RawRequest, RateLimitStore, RateLimitResult, RateLimitConfig, PriceInterval, PlanVersionStore, PlanVersionInfo, PlanSnapshot, PlanPrice, PlanHashInput, PlanDef, Period, PasswordResetStore, PasswordResetConfig, PasswordRequirements, ParentRef, OverrideStore, OverageInput, OverageConfig, OnUserCreatedPayload, OAuthUserInfo, OAuthTokens, OAuthProviderConfig, OAuthProvider, OAuthAccountStore, NotFoundException, NamedMiddlewareDef, MiddlewareDef, MfaSetupData, MfaConfig, MfaChallengeData, MFAStore, ListenOptions, ListResult, ListOptions2 as ListOptions, LimitOverrideDef, LimitOverride, LimitDef, InternalServerErrorException, InferSchema, Infer2 as Infer, InMemoryWalletStore, InMemoryUserStore, InMemorySubscriptionStore, InMemorySessionStore, InMemoryRoleAssignmentStore, InMemoryRateLimitStore, InMemoryPlanVersionStore, InMemoryPasswordResetStore, InMemoryOverrideStore, InMemoryOAuthAccountStore, InMemoryMFAStore, InMemoryGrandfatheringStore, InMemoryFlagStore, InMemoryEmailVerificationStore, InMemoryClosureStore, HttpStatusCode, HttpMethod, HandlerCtx, GrandfatheringStore, GrandfatheringState, ForbiddenException, FlagStore, ExposeConfig, EnvConfig, EntityRouteOptions, EntityRelationsConfig, EntityRegistry, EntityOperations, EntityErrorResult, EntityDefinition, EntityDef, EntityDbAdapter2 as EntityDbAdapter, EntityContext, EntityConfig, EntityActionDef, EntitlementValue, EntitlementRegistry, EntitlementDefinition, EntitlementDef, Entitlement, EncodedAccessSet, EmailVerificationStore, EmailVerificationConfig, EmailPasswordConfig, Deps, DenialReason, DenialMeta, DefineAccessInput, DeepReadonly, DbUserStore, DbSubscriptionStore, DbSessionStore, DbRoleAssignmentStore, DbOAuthAccountStore, DbFlagStore, DbDialectName, DbClosureStore, Ctx, CrudResult, CrudHandlers, CorsConfig, CookieConfig, ContentDescriptor, ConsumeResult, ConflictException, ComputeAccessSetConfig, ClosureStore, ClosureRow, ClosureEntry, BillingPeriod, BillingEventType, BillingEventHandler, BillingEventEmitter, BillingEvent, BillingAdapter, BaseContext, BadRequestException, AuthorizationError, AuthUser, AuthTokens, AuthInstance, AuthEntityProxy, AuthDbClient, AuthContext, AuthConfig, AuthCallbackContext, AuthApi, AppConfig2 as AppConfig, AppBuilder2 as AppBuilder, AddOnRequires, AclClaim, AccumulateProvides, AccessWsData, AccessSet, AccessRule2 as AccessRule, AccessInstance, AccessEventBroadcasterConfig, AccessEventBroadcaster, AccessEvent, AccessDefinition, AccessContextConfig, AccessContext, AccessConfig, AccessCheckResult, AccessCheckData, AUTH_TABLE_NAMES };
|
package/dist/index.js
CHANGED
|
@@ -5992,6 +5992,15 @@ function applySelect(select, data) {
|
|
|
5992
5992
|
}
|
|
5993
5993
|
return result;
|
|
5994
5994
|
}
|
|
5995
|
+
function nullGuardedFields(nulledFields, data) {
|
|
5996
|
+
if (nulledFields.size === 0)
|
|
5997
|
+
return data;
|
|
5998
|
+
const result = {};
|
|
5999
|
+
for (const [key, value] of Object.entries(data)) {
|
|
6000
|
+
result[key] = nulledFields.has(key) ? null : value;
|
|
6001
|
+
}
|
|
6002
|
+
return result;
|
|
6003
|
+
}
|
|
5995
6004
|
function stripReadOnlyFields(table, data) {
|
|
5996
6005
|
const excludedKeys = new Set;
|
|
5997
6006
|
for (const key of Object.keys(table._columns)) {
|
|
@@ -6227,6 +6236,260 @@ import {
|
|
|
6227
6236
|
err as err4,
|
|
6228
6237
|
ok as ok4
|
|
6229
6238
|
} from "@vertz/errors";
|
|
6239
|
+
|
|
6240
|
+
// src/entity/vertzql-parser.ts
|
|
6241
|
+
function extractAllowKeys(allow) {
|
|
6242
|
+
if (!allow)
|
|
6243
|
+
return [];
|
|
6244
|
+
if (Array.isArray(allow))
|
|
6245
|
+
return allow;
|
|
6246
|
+
return Object.keys(allow);
|
|
6247
|
+
}
|
|
6248
|
+
var MAX_CURSOR_LENGTH = 512;
|
|
6249
|
+
var MAX_LIMIT = 1000;
|
|
6250
|
+
var MAX_Q_BASE64_LENGTH = 10240;
|
|
6251
|
+
var ALLOWED_Q_KEYS = new Set(["select", "include", "where", "orderBy", "limit"]);
|
|
6252
|
+
function parseVertzQL(query) {
|
|
6253
|
+
const result = {};
|
|
6254
|
+
for (const [key, value] of Object.entries(query)) {
|
|
6255
|
+
const whereMatch = key.match(/^where\[([^\]]+)\](?:\[([^\]]+)\])?$/);
|
|
6256
|
+
if (whereMatch) {
|
|
6257
|
+
if (!result.where)
|
|
6258
|
+
result.where = {};
|
|
6259
|
+
const field = whereMatch[1];
|
|
6260
|
+
const op = whereMatch[2];
|
|
6261
|
+
const existing = result.where[field];
|
|
6262
|
+
if (op) {
|
|
6263
|
+
const base = existing && typeof existing === "object" ? existing : existing !== undefined ? { eq: existing } : {};
|
|
6264
|
+
result.where[field] = { ...base, [op]: value };
|
|
6265
|
+
} else {
|
|
6266
|
+
if (existing && typeof existing === "object") {
|
|
6267
|
+
result.where[field] = { ...existing, eq: value };
|
|
6268
|
+
} else {
|
|
6269
|
+
result.where[field] = value;
|
|
6270
|
+
}
|
|
6271
|
+
}
|
|
6272
|
+
continue;
|
|
6273
|
+
}
|
|
6274
|
+
if (key === "limit") {
|
|
6275
|
+
const parsed = Number.parseInt(value, 10);
|
|
6276
|
+
if (!Number.isNaN(parsed)) {
|
|
6277
|
+
result.limit = Math.max(0, Math.min(parsed, MAX_LIMIT));
|
|
6278
|
+
}
|
|
6279
|
+
continue;
|
|
6280
|
+
}
|
|
6281
|
+
if (key === "after") {
|
|
6282
|
+
if (value && value.length <= MAX_CURSOR_LENGTH) {
|
|
6283
|
+
result.after = value;
|
|
6284
|
+
}
|
|
6285
|
+
continue;
|
|
6286
|
+
}
|
|
6287
|
+
if (key === "orderBy") {
|
|
6288
|
+
const [field, dir] = value.split(":");
|
|
6289
|
+
if (field) {
|
|
6290
|
+
if (!result.orderBy)
|
|
6291
|
+
result.orderBy = {};
|
|
6292
|
+
result.orderBy[field] = dir === "desc" ? "desc" : "asc";
|
|
6293
|
+
}
|
|
6294
|
+
continue;
|
|
6295
|
+
}
|
|
6296
|
+
if (key === "q") {
|
|
6297
|
+
try {
|
|
6298
|
+
const urlDecoded = decodeURIComponent(value);
|
|
6299
|
+
if (urlDecoded.length > MAX_Q_BASE64_LENGTH) {
|
|
6300
|
+
result._qError = "q= parameter exceeds maximum allowed size";
|
|
6301
|
+
continue;
|
|
6302
|
+
}
|
|
6303
|
+
const b64 = urlDecoded.replace(/-/g, "+").replace(/_/g, "/");
|
|
6304
|
+
const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
|
|
6305
|
+
const decoded = JSON.parse(atob(padded));
|
|
6306
|
+
for (const k of Object.keys(decoded)) {
|
|
6307
|
+
if (!ALLOWED_Q_KEYS.has(k)) {
|
|
6308
|
+
delete decoded[k];
|
|
6309
|
+
}
|
|
6310
|
+
}
|
|
6311
|
+
if (decoded.select && typeof decoded.select === "object") {
|
|
6312
|
+
result.select = decoded.select;
|
|
6313
|
+
}
|
|
6314
|
+
if (decoded.include && typeof decoded.include === "object") {
|
|
6315
|
+
result.include = decoded.include;
|
|
6316
|
+
}
|
|
6317
|
+
if (decoded.where && typeof decoded.where === "object" && !Array.isArray(decoded.where)) {
|
|
6318
|
+
result.where = { ...result.where, ...decoded.where };
|
|
6319
|
+
}
|
|
6320
|
+
if (decoded.orderBy && typeof decoded.orderBy === "object" && !Array.isArray(decoded.orderBy)) {
|
|
6321
|
+
const orderByObj = decoded.orderBy;
|
|
6322
|
+
const sanitized = {};
|
|
6323
|
+
for (const [field, dir] of Object.entries(orderByObj)) {
|
|
6324
|
+
sanitized[field] = dir === "desc" ? "desc" : "asc";
|
|
6325
|
+
}
|
|
6326
|
+
result.orderBy = { ...result.orderBy, ...sanitized };
|
|
6327
|
+
}
|
|
6328
|
+
if (decoded.limit !== undefined) {
|
|
6329
|
+
const parsed = typeof decoded.limit === "number" ? decoded.limit : Number(decoded.limit);
|
|
6330
|
+
if (!Number.isNaN(parsed)) {
|
|
6331
|
+
result.limit = Math.max(0, Math.min(parsed, MAX_LIMIT));
|
|
6332
|
+
}
|
|
6333
|
+
}
|
|
6334
|
+
} catch {
|
|
6335
|
+
result._qError = "Invalid q= parameter: not valid base64 or JSON";
|
|
6336
|
+
}
|
|
6337
|
+
}
|
|
6338
|
+
}
|
|
6339
|
+
return result;
|
|
6340
|
+
}
|
|
6341
|
+
function getHiddenColumns(table) {
|
|
6342
|
+
const hidden = new Set;
|
|
6343
|
+
for (const key of Object.keys(table._columns)) {
|
|
6344
|
+
const col = table._columns[key];
|
|
6345
|
+
if (col?._meta._annotations.hidden) {
|
|
6346
|
+
hidden.add(key);
|
|
6347
|
+
}
|
|
6348
|
+
}
|
|
6349
|
+
return hidden;
|
|
6350
|
+
}
|
|
6351
|
+
function validateVertzQL(options, table, relationsConfig, exposeConfig, evaluatedExpose) {
|
|
6352
|
+
if (options._qError) {
|
|
6353
|
+
return { ok: false, error: options._qError };
|
|
6354
|
+
}
|
|
6355
|
+
const hiddenColumns = getHiddenColumns(table);
|
|
6356
|
+
if (options.where) {
|
|
6357
|
+
const allowWhereSet = evaluatedExpose ? evaluatedExpose.allowedWhereFields : null;
|
|
6358
|
+
const allowWhereKeys = !evaluatedExpose && exposeConfig ? extractAllowKeys(exposeConfig.allowWhere) : null;
|
|
6359
|
+
for (const field of Object.keys(options.where)) {
|
|
6360
|
+
if (hiddenColumns.has(field)) {
|
|
6361
|
+
return { ok: false, error: `Field "${field}" is not filterable` };
|
|
6362
|
+
}
|
|
6363
|
+
if (allowWhereSet !== null && !allowWhereSet.has(field)) {
|
|
6364
|
+
return { ok: false, error: `Field "${field}" is not filterable` };
|
|
6365
|
+
}
|
|
6366
|
+
if (allowWhereKeys !== null && !allowWhereKeys.includes(field)) {
|
|
6367
|
+
return { ok: false, error: `Field "${field}" is not filterable` };
|
|
6368
|
+
}
|
|
6369
|
+
}
|
|
6370
|
+
}
|
|
6371
|
+
if (options.orderBy) {
|
|
6372
|
+
const allowOrderBySet = evaluatedExpose ? evaluatedExpose.allowedOrderByFields : null;
|
|
6373
|
+
const allowOrderByKeys = !evaluatedExpose && exposeConfig ? extractAllowKeys(exposeConfig.allowOrderBy) : null;
|
|
6374
|
+
for (const field of Object.keys(options.orderBy)) {
|
|
6375
|
+
if (hiddenColumns.has(field)) {
|
|
6376
|
+
return { ok: false, error: `Field "${field}" is not sortable` };
|
|
6377
|
+
}
|
|
6378
|
+
if (allowOrderBySet !== null && !allowOrderBySet.has(field)) {
|
|
6379
|
+
return { ok: false, error: `Field "${field}" is not sortable` };
|
|
6380
|
+
}
|
|
6381
|
+
if (allowOrderByKeys !== null && !allowOrderByKeys.includes(field)) {
|
|
6382
|
+
return { ok: false, error: `Field "${field}" is not sortable` };
|
|
6383
|
+
}
|
|
6384
|
+
}
|
|
6385
|
+
}
|
|
6386
|
+
if (options.select) {
|
|
6387
|
+
const exposeSelectKeys = exposeConfig ? extractAllowKeys(exposeConfig.select) : null;
|
|
6388
|
+
for (const field of Object.keys(options.select)) {
|
|
6389
|
+
if (hiddenColumns.has(field)) {
|
|
6390
|
+
return { ok: false, error: `Field "${field}" is not selectable` };
|
|
6391
|
+
}
|
|
6392
|
+
if (exposeSelectKeys !== null && exposeSelectKeys.length > 0 && !exposeSelectKeys.includes(field)) {
|
|
6393
|
+
return { ok: false, error: `Field "${field}" is not selectable` };
|
|
6394
|
+
}
|
|
6395
|
+
}
|
|
6396
|
+
}
|
|
6397
|
+
if (options.include && relationsConfig) {
|
|
6398
|
+
const includeResult = validateInclude(options.include, relationsConfig, "");
|
|
6399
|
+
if (!includeResult.ok)
|
|
6400
|
+
return includeResult;
|
|
6401
|
+
}
|
|
6402
|
+
return { ok: true };
|
|
6403
|
+
}
|
|
6404
|
+
function validateInclude(include, relationsConfig, pathPrefix) {
|
|
6405
|
+
for (const [relation, requested] of Object.entries(include)) {
|
|
6406
|
+
const entityConfig = relationsConfig[relation];
|
|
6407
|
+
const relationPath = pathPrefix ? `${pathPrefix}.${relation}` : relation;
|
|
6408
|
+
if (entityConfig === undefined || entityConfig === false) {
|
|
6409
|
+
return { ok: false, error: `Relation "${relationPath}" is not exposed` };
|
|
6410
|
+
}
|
|
6411
|
+
if (requested === true)
|
|
6412
|
+
continue;
|
|
6413
|
+
const configObj = typeof entityConfig === "object" ? entityConfig : undefined;
|
|
6414
|
+
if (requested.where) {
|
|
6415
|
+
const allowWhereKeys = extractAllowKeys(configObj?.allowWhere);
|
|
6416
|
+
if (!configObj || allowWhereKeys.length === 0) {
|
|
6417
|
+
return {
|
|
6418
|
+
ok: false,
|
|
6419
|
+
error: `Filtering is not enabled on relation '${relationPath}'. ` + "Add 'allowWhere' to the entity relations config."
|
|
6420
|
+
};
|
|
6421
|
+
}
|
|
6422
|
+
const allowedSet = new Set(allowWhereKeys);
|
|
6423
|
+
for (const field of Object.keys(requested.where)) {
|
|
6424
|
+
if (!allowedSet.has(field)) {
|
|
6425
|
+
return {
|
|
6426
|
+
ok: false,
|
|
6427
|
+
error: `Field '${field}' is not filterable on relation '${relationPath}'. ` + `Allowed: ${allowWhereKeys.join(", ")}`
|
|
6428
|
+
};
|
|
6429
|
+
}
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
6432
|
+
if (requested.orderBy) {
|
|
6433
|
+
const allowOrderByKeys = extractAllowKeys(configObj?.allowOrderBy);
|
|
6434
|
+
if (!configObj || allowOrderByKeys.length === 0) {
|
|
6435
|
+
return {
|
|
6436
|
+
ok: false,
|
|
6437
|
+
error: `Sorting is not enabled on relation '${relationPath}'. ` + "Add 'allowOrderBy' to the entity relations config."
|
|
6438
|
+
};
|
|
6439
|
+
}
|
|
6440
|
+
const allowedSet = new Set(allowOrderByKeys);
|
|
6441
|
+
for (const [field, dir] of Object.entries(requested.orderBy)) {
|
|
6442
|
+
if (!allowedSet.has(field)) {
|
|
6443
|
+
return {
|
|
6444
|
+
ok: false,
|
|
6445
|
+
error: `Field '${field}' is not sortable on relation '${relationPath}'. ` + `Allowed: ${allowOrderByKeys.join(", ")}`
|
|
6446
|
+
};
|
|
6447
|
+
}
|
|
6448
|
+
if (dir !== "asc" && dir !== "desc") {
|
|
6449
|
+
return {
|
|
6450
|
+
ok: false,
|
|
6451
|
+
error: `Invalid orderBy direction '${String(dir)}' for field '${field}' on relation '${relationPath}'. Must be 'asc' or 'desc'.`
|
|
6452
|
+
};
|
|
6453
|
+
}
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6456
|
+
if (requested.limit !== undefined) {
|
|
6457
|
+
if (typeof requested.limit !== "number" || !Number.isFinite(requested.limit)) {
|
|
6458
|
+
return {
|
|
6459
|
+
ok: false,
|
|
6460
|
+
error: `Invalid limit on relation '${relationPath}': must be a finite number`
|
|
6461
|
+
};
|
|
6462
|
+
}
|
|
6463
|
+
if (requested.limit < 0) {
|
|
6464
|
+
requested.limit = 0;
|
|
6465
|
+
}
|
|
6466
|
+
if (configObj?.maxLimit !== undefined && requested.limit > configObj.maxLimit) {
|
|
6467
|
+
requested.limit = configObj.maxLimit;
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
6470
|
+
if (requested.select && configObj?.select) {
|
|
6471
|
+
for (const field of Object.keys(requested.select)) {
|
|
6472
|
+
if (!(field in configObj.select)) {
|
|
6473
|
+
return {
|
|
6474
|
+
ok: false,
|
|
6475
|
+
error: `Field "${field}" is not exposed on relation "${relationPath}"`
|
|
6476
|
+
};
|
|
6477
|
+
}
|
|
6478
|
+
}
|
|
6479
|
+
}
|
|
6480
|
+
if (requested.include) {
|
|
6481
|
+
if (entityConfig === true) {
|
|
6482
|
+
return {
|
|
6483
|
+
ok: false,
|
|
6484
|
+
error: `Nested includes are not supported on relation '${relationPath}' ` + "without a structured relations config."
|
|
6485
|
+
};
|
|
6486
|
+
}
|
|
6487
|
+
}
|
|
6488
|
+
}
|
|
6489
|
+
return { ok: true };
|
|
6490
|
+
}
|
|
6491
|
+
|
|
6492
|
+
// src/entity/crud-pipeline.ts
|
|
6230
6493
|
function resolvePrimaryKeyColumn(table) {
|
|
6231
6494
|
for (const key of Object.keys(table._columns)) {
|
|
6232
6495
|
const col = table._columns[key];
|
|
@@ -6241,6 +6504,10 @@ function createCrudHandlers(def, db, options) {
|
|
|
6241
6504
|
const tenantChain = options?.tenantChain ?? def.tenantChain ?? null;
|
|
6242
6505
|
const isIndirectlyScoped = tenantChain !== null;
|
|
6243
6506
|
const queryParentIds = options?.queryParentIds ?? null;
|
|
6507
|
+
const exposeSelect = def.expose?.select ? {
|
|
6508
|
+
...def.expose.select,
|
|
6509
|
+
...Object.fromEntries(Object.entries(def.expose.include ?? {}).filter(([, v]) => v !== false).map(([k]) => [k, true]))
|
|
6510
|
+
} : undefined;
|
|
6244
6511
|
function notFound(id) {
|
|
6245
6512
|
return err4(new EntityNotFoundError2(`${def.name} with id "${id}" not found`));
|
|
6246
6513
|
}
|
|
@@ -6307,11 +6574,11 @@ function createCrudHandlers(def, db, options) {
|
|
|
6307
6574
|
const indirectWhere = await resolveIndirectTenantWhere(ctx);
|
|
6308
6575
|
const where = indirectWhere ? { ...directWhere, ...indirectWhere } : directWhere;
|
|
6309
6576
|
const limit = Math.max(0, options2?.limit ?? 20);
|
|
6310
|
-
const after = options2?.after && options2.after.length <=
|
|
6577
|
+
const after = options2?.after && options2.after.length <= MAX_CURSOR_LENGTH ? options2.after : undefined;
|
|
6311
6578
|
const orderBy = options2?.orderBy;
|
|
6312
6579
|
const include = options2?.include;
|
|
6313
6580
|
const { data: rows, total } = await db.list({ where, orderBy, limit, after, include });
|
|
6314
|
-
const data = rows.map((row) => narrowRelationFields(def.
|
|
6581
|
+
const data = rows.map((row) => applySelect(exposeSelect, narrowRelationFields(def.expose?.include ?? {}, stripHiddenFields(table, row))));
|
|
6315
6582
|
const pkColumn = resolvePrimaryKeyColumn(table);
|
|
6316
6583
|
const lastRow = rows[rows.length - 1];
|
|
6317
6584
|
const nextCursor = limit > 0 && rows.length === limit && lastRow ? String(lastRow[pkColumn]) : null;
|
|
@@ -6333,7 +6600,7 @@ function createCrudHandlers(def, db, options) {
|
|
|
6333
6600
|
return err4(accessResult.error);
|
|
6334
6601
|
return ok4({
|
|
6335
6602
|
status: 200,
|
|
6336
|
-
body: narrowRelationFields(def.
|
|
6603
|
+
body: applySelect(exposeSelect, narrowRelationFields(def.expose?.include ?? {}, stripHiddenFields(table, row)))
|
|
6337
6604
|
});
|
|
6338
6605
|
},
|
|
6339
6606
|
async create(ctx, data) {
|
|
@@ -6376,7 +6643,7 @@ function createCrudHandlers(def, db, options) {
|
|
|
6376
6643
|
}
|
|
6377
6644
|
return ok4({
|
|
6378
6645
|
status: 201,
|
|
6379
|
-
body: narrowRelationFields(def.
|
|
6646
|
+
body: applySelect(exposeSelect, narrowRelationFields(def.expose?.include ?? {}, strippedResult))
|
|
6380
6647
|
});
|
|
6381
6648
|
},
|
|
6382
6649
|
async update(ctx, id, data) {
|
|
@@ -6405,7 +6672,7 @@ function createCrudHandlers(def, db, options) {
|
|
|
6405
6672
|
}
|
|
6406
6673
|
return ok4({
|
|
6407
6674
|
status: 200,
|
|
6408
|
-
body: narrowRelationFields(def.
|
|
6675
|
+
body: applySelect(exposeSelect, narrowRelationFields(def.expose?.include ?? {}, strippedResult))
|
|
6409
6676
|
});
|
|
6410
6677
|
},
|
|
6411
6678
|
async delete(ctx, id) {
|
|
@@ -6493,209 +6760,91 @@ function entityErrorHandler(error) {
|
|
|
6493
6760
|
};
|
|
6494
6761
|
}
|
|
6495
6762
|
|
|
6496
|
-
// src/entity/
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
const existing = result.where[field];
|
|
6510
|
-
if (op) {
|
|
6511
|
-
const base = existing && typeof existing === "object" ? existing : existing !== undefined ? { eq: existing } : {};
|
|
6512
|
-
result.where[field] = { ...base, [op]: value };
|
|
6513
|
-
} else {
|
|
6514
|
-
if (existing && typeof existing === "object") {
|
|
6515
|
-
result.where[field] = { ...existing, eq: value };
|
|
6516
|
-
} else {
|
|
6517
|
-
result.where[field] = value;
|
|
6518
|
-
}
|
|
6519
|
-
}
|
|
6520
|
-
continue;
|
|
6521
|
-
}
|
|
6522
|
-
if (key === "limit") {
|
|
6523
|
-
const parsed = Number.parseInt(value, 10);
|
|
6524
|
-
if (!Number.isNaN(parsed)) {
|
|
6525
|
-
result.limit = Math.max(0, Math.min(parsed, MAX_LIMIT));
|
|
6526
|
-
}
|
|
6527
|
-
continue;
|
|
6528
|
-
}
|
|
6529
|
-
if (key === "after") {
|
|
6530
|
-
if (value) {
|
|
6531
|
-
result.after = value;
|
|
6532
|
-
}
|
|
6533
|
-
continue;
|
|
6763
|
+
// src/entity/expose-evaluator.ts
|
|
6764
|
+
async function evaluateExposeRule(rule, ctx, options) {
|
|
6765
|
+
switch (rule.type) {
|
|
6766
|
+
case "public":
|
|
6767
|
+
return true;
|
|
6768
|
+
case "authenticated":
|
|
6769
|
+
return ctx.authenticated();
|
|
6770
|
+
case "role":
|
|
6771
|
+
return ctx.role(...rule.roles);
|
|
6772
|
+
case "entitlement": {
|
|
6773
|
+
if (!options.can)
|
|
6774
|
+
return false;
|
|
6775
|
+
return options.can(rule.entitlement);
|
|
6534
6776
|
}
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6777
|
+
case "where":
|
|
6778
|
+
return false;
|
|
6779
|
+
case "all": {
|
|
6780
|
+
for (const sub of rule.rules) {
|
|
6781
|
+
if (!await evaluateExposeRule(sub, ctx, options))
|
|
6782
|
+
return false;
|
|
6541
6783
|
}
|
|
6542
|
-
|
|
6784
|
+
return true;
|
|
6543
6785
|
}
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
result._qError = "q= parameter exceeds maximum allowed size";
|
|
6549
|
-
continue;
|
|
6550
|
-
}
|
|
6551
|
-
const b64 = urlDecoded.replace(/-/g, "+").replace(/_/g, "/");
|
|
6552
|
-
const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
|
|
6553
|
-
const decoded = JSON.parse(atob(padded));
|
|
6554
|
-
for (const k of Object.keys(decoded)) {
|
|
6555
|
-
if (!ALLOWED_Q_KEYS.has(k)) {
|
|
6556
|
-
delete decoded[k];
|
|
6557
|
-
}
|
|
6558
|
-
}
|
|
6559
|
-
if (decoded.select && typeof decoded.select === "object") {
|
|
6560
|
-
result.select = decoded.select;
|
|
6561
|
-
}
|
|
6562
|
-
if (decoded.include && typeof decoded.include === "object") {
|
|
6563
|
-
result.include = decoded.include;
|
|
6564
|
-
}
|
|
6565
|
-
} catch {
|
|
6566
|
-
result._qError = "Invalid q= parameter: not valid base64 or JSON";
|
|
6786
|
+
case "any": {
|
|
6787
|
+
for (const sub of rule.rules) {
|
|
6788
|
+
if (await evaluateExposeRule(sub, ctx, options))
|
|
6789
|
+
return true;
|
|
6567
6790
|
}
|
|
6791
|
+
return false;
|
|
6568
6792
|
}
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
const hidden = new Set;
|
|
6574
|
-
for (const key of Object.keys(table._columns)) {
|
|
6575
|
-
const col = table._columns[key];
|
|
6576
|
-
if (col?._meta._annotations.hidden) {
|
|
6577
|
-
hidden.add(key);
|
|
6793
|
+
case "fva": {
|
|
6794
|
+
if (options.fvaAge === undefined)
|
|
6795
|
+
return false;
|
|
6796
|
+
return options.fvaAge <= rule.maxAge;
|
|
6578
6797
|
}
|
|
6579
6798
|
}
|
|
6580
|
-
return hidden;
|
|
6581
6799
|
}
|
|
6582
|
-
function
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
const
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
return { ok: false, error: `Field "${field}" is not sortable` };
|
|
6598
|
-
}
|
|
6599
|
-
}
|
|
6600
|
-
}
|
|
6601
|
-
if (options.select) {
|
|
6602
|
-
for (const field of Object.keys(options.select)) {
|
|
6603
|
-
if (hiddenColumns.has(field)) {
|
|
6604
|
-
return { ok: false, error: `Field "${field}" is not selectable` };
|
|
6800
|
+
async function evaluateExposeDescriptors(expose, ctx, options = {}) {
|
|
6801
|
+
const allowedSelectFields = new Set;
|
|
6802
|
+
const nulledFields = new Set;
|
|
6803
|
+
const allowedWhereFields = new Set;
|
|
6804
|
+
const allowedOrderByFields = new Set;
|
|
6805
|
+
for (const [field, value] of Object.entries(expose.select)) {
|
|
6806
|
+
if (value === true) {
|
|
6807
|
+
allowedSelectFields.add(field);
|
|
6808
|
+
} else {
|
|
6809
|
+
const passed = await evaluateExposeRule(value, ctx, options);
|
|
6810
|
+
if (passed) {
|
|
6811
|
+
allowedSelectFields.add(field);
|
|
6812
|
+
} else {
|
|
6813
|
+
allowedSelectFields.add(field);
|
|
6814
|
+
nulledFields.add(field);
|
|
6605
6815
|
}
|
|
6606
6816
|
}
|
|
6607
6817
|
}
|
|
6608
|
-
if (
|
|
6609
|
-
const
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
for (const [relation, requested] of Object.entries(include)) {
|
|
6617
|
-
const entityConfig = relationsConfig[relation];
|
|
6618
|
-
const relationPath = pathPrefix ? `${pathPrefix}.${relation}` : relation;
|
|
6619
|
-
if (entityConfig === undefined || entityConfig === false) {
|
|
6620
|
-
return { ok: false, error: `Relation "${relationPath}" is not exposed` };
|
|
6621
|
-
}
|
|
6622
|
-
if (requested === true)
|
|
6623
|
-
continue;
|
|
6624
|
-
const configObj = typeof entityConfig === "object" ? entityConfig : undefined;
|
|
6625
|
-
if (requested.where) {
|
|
6626
|
-
if (!configObj || !configObj.allowWhere || configObj.allowWhere.length === 0) {
|
|
6627
|
-
return {
|
|
6628
|
-
ok: false,
|
|
6629
|
-
error: `Filtering is not enabled on relation '${relationPath}'. ` + "Add 'allowWhere' to the entity relations config."
|
|
6630
|
-
};
|
|
6631
|
-
}
|
|
6632
|
-
const allowedSet = new Set(configObj.allowWhere);
|
|
6633
|
-
for (const field of Object.keys(requested.where)) {
|
|
6634
|
-
if (!allowedSet.has(field)) {
|
|
6635
|
-
return {
|
|
6636
|
-
ok: false,
|
|
6637
|
-
error: `Field '${field}' is not filterable on relation '${relationPath}'. ` + `Allowed: ${configObj.allowWhere.join(", ")}`
|
|
6638
|
-
};
|
|
6639
|
-
}
|
|
6640
|
-
}
|
|
6641
|
-
}
|
|
6642
|
-
if (requested.orderBy) {
|
|
6643
|
-
if (!configObj || !configObj.allowOrderBy || configObj.allowOrderBy.length === 0) {
|
|
6644
|
-
return {
|
|
6645
|
-
ok: false,
|
|
6646
|
-
error: `Sorting is not enabled on relation '${relationPath}'. ` + "Add 'allowOrderBy' to the entity relations config."
|
|
6647
|
-
};
|
|
6648
|
-
}
|
|
6649
|
-
const allowedSet = new Set(configObj.allowOrderBy);
|
|
6650
|
-
for (const [field, dir] of Object.entries(requested.orderBy)) {
|
|
6651
|
-
if (!allowedSet.has(field)) {
|
|
6652
|
-
return {
|
|
6653
|
-
ok: false,
|
|
6654
|
-
error: `Field '${field}' is not sortable on relation '${relationPath}'. ` + `Allowed: ${configObj.allowOrderBy.join(", ")}`
|
|
6655
|
-
};
|
|
6656
|
-
}
|
|
6657
|
-
if (dir !== "asc" && dir !== "desc") {
|
|
6658
|
-
return {
|
|
6659
|
-
ok: false,
|
|
6660
|
-
error: `Invalid orderBy direction '${String(dir)}' for field '${field}' on relation '${relationPath}'. Must be 'asc' or 'desc'.`
|
|
6661
|
-
};
|
|
6818
|
+
if (expose.allowWhere) {
|
|
6819
|
+
for (const [field, value] of Object.entries(expose.allowWhere)) {
|
|
6820
|
+
if (value === true) {
|
|
6821
|
+
allowedWhereFields.add(field);
|
|
6822
|
+
} else {
|
|
6823
|
+
const passed = await evaluateExposeRule(value, ctx, options);
|
|
6824
|
+
if (passed) {
|
|
6825
|
+
allowedWhereFields.add(field);
|
|
6662
6826
|
}
|
|
6663
6827
|
}
|
|
6664
6828
|
}
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
}
|
|
6675
|
-
if (configObj?.maxLimit !== undefined && requested.limit > configObj.maxLimit) {
|
|
6676
|
-
requested.limit = configObj.maxLimit;
|
|
6677
|
-
}
|
|
6678
|
-
}
|
|
6679
|
-
if (requested.select && configObj?.select) {
|
|
6680
|
-
for (const field of Object.keys(requested.select)) {
|
|
6681
|
-
if (!(field in configObj.select)) {
|
|
6682
|
-
return {
|
|
6683
|
-
ok: false,
|
|
6684
|
-
error: `Field "${field}" is not exposed on relation "${relationPath}"`
|
|
6685
|
-
};
|
|
6829
|
+
}
|
|
6830
|
+
if (expose.allowOrderBy) {
|
|
6831
|
+
for (const [field, value] of Object.entries(expose.allowOrderBy)) {
|
|
6832
|
+
if (value === true) {
|
|
6833
|
+
allowedOrderByFields.add(field);
|
|
6834
|
+
} else {
|
|
6835
|
+
const passed = await evaluateExposeRule(value, ctx, options);
|
|
6836
|
+
if (passed) {
|
|
6837
|
+
allowedOrderByFields.add(field);
|
|
6686
6838
|
}
|
|
6687
6839
|
}
|
|
6688
6840
|
}
|
|
6689
|
-
if (requested.include) {
|
|
6690
|
-
if (entityConfig === true) {
|
|
6691
|
-
return {
|
|
6692
|
-
ok: false,
|
|
6693
|
-
error: `Nested includes are not supported on relation '${relationPath}' ` + "without a structured relations config."
|
|
6694
|
-
};
|
|
6695
|
-
}
|
|
6696
|
-
}
|
|
6697
6841
|
}
|
|
6698
|
-
return {
|
|
6842
|
+
return {
|
|
6843
|
+
allowedSelectFields,
|
|
6844
|
+
nulledFields,
|
|
6845
|
+
allowedWhereFields,
|
|
6846
|
+
allowedOrderByFields
|
|
6847
|
+
};
|
|
6699
6848
|
}
|
|
6700
6849
|
|
|
6701
6850
|
// src/entity/route-generator.ts
|
|
@@ -6718,10 +6867,27 @@ function extractRequestInfo(ctx) {
|
|
|
6718
6867
|
function getParams(ctx) {
|
|
6719
6868
|
return ctx.params ?? {};
|
|
6720
6869
|
}
|
|
6870
|
+
function hasDescriptorValues(record) {
|
|
6871
|
+
if (!record)
|
|
6872
|
+
return false;
|
|
6873
|
+
return Object.values(record).some((v) => v !== true);
|
|
6874
|
+
}
|
|
6875
|
+
function hasDescriptors(expose) {
|
|
6876
|
+
return hasDescriptorValues(expose.select) || hasDescriptorValues(expose.allowWhere) || hasDescriptorValues(expose.allowOrderBy);
|
|
6877
|
+
}
|
|
6878
|
+
function applyNulling(data, nulledFields) {
|
|
6879
|
+
return nullGuardedFields(nulledFields, data);
|
|
6880
|
+
}
|
|
6721
6881
|
function generateEntityRoutes(def, registry, db, options) {
|
|
6722
6882
|
const prefix = options?.apiPrefix ?? "/api";
|
|
6723
6883
|
const basePath = `${prefix}/${def.name}`;
|
|
6724
6884
|
const tenantChain = options?.tenantChain ?? null;
|
|
6885
|
+
const exposeValidation = def.expose ? {
|
|
6886
|
+
select: def.expose.select,
|
|
6887
|
+
allowWhere: def.expose.allowWhere,
|
|
6888
|
+
allowOrderBy: def.expose.allowOrderBy
|
|
6889
|
+
} : undefined;
|
|
6890
|
+
const exposeEvalConfig = def.expose ? hasDescriptors(def.expose) ? def.expose : null : null;
|
|
6725
6891
|
const crudHandlers = createCrudHandlers(def, db, {
|
|
6726
6892
|
tenantChain,
|
|
6727
6893
|
queryParentIds: options?.queryParentIds
|
|
@@ -6755,8 +6921,9 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6755
6921
|
const entityCtx = makeEntityCtx(ctx);
|
|
6756
6922
|
const query = ctx.query ?? {};
|
|
6757
6923
|
const parsed = parseVertzQL(query);
|
|
6758
|
-
const
|
|
6759
|
-
const
|
|
6924
|
+
const evaluated = exposeEvalConfig ? await evaluateExposeDescriptors(exposeEvalConfig, entityCtx) : null;
|
|
6925
|
+
const relationsConfig = def.expose?.include ?? {};
|
|
6926
|
+
const validation = validateVertzQL(parsed, def.model.table, relationsConfig, exposeValidation, evaluated ?? undefined);
|
|
6760
6927
|
if (!validation.ok) {
|
|
6761
6928
|
return jsonResponse({ error: { code: "BadRequest", message: validation.error } }, 400);
|
|
6762
6929
|
}
|
|
@@ -6772,6 +6939,9 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6772
6939
|
const { status, body } = entityErrorHandler(result.error);
|
|
6773
6940
|
return jsonResponse(body, status);
|
|
6774
6941
|
}
|
|
6942
|
+
if (evaluated && evaluated.nulledFields.size > 0 && result.data.body.items) {
|
|
6943
|
+
result.data.body.items = result.data.body.items.map((row) => applyNulling(row, evaluated.nulledFields));
|
|
6944
|
+
}
|
|
6775
6945
|
if (parsed.select && result.data.body.items) {
|
|
6776
6946
|
result.data.body.items = result.data.body.items.map((row) => applySelect(parsed.select, row));
|
|
6777
6947
|
}
|
|
@@ -6790,6 +6960,19 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6790
6960
|
try {
|
|
6791
6961
|
const entityCtx = makeEntityCtx(ctx);
|
|
6792
6962
|
const body = ctx.body ?? {};
|
|
6963
|
+
if (body.after !== undefined) {
|
|
6964
|
+
if (typeof body.after !== "string") {
|
|
6965
|
+
return jsonResponse({ error: { code: "BadRequest", message: "cursor must be a string" } }, 400);
|
|
6966
|
+
}
|
|
6967
|
+
if (body.after.length > MAX_CURSOR_LENGTH) {
|
|
6968
|
+
return jsonResponse({
|
|
6969
|
+
error: {
|
|
6970
|
+
code: "BadRequest",
|
|
6971
|
+
message: `cursor exceeds maximum length of ${MAX_CURSOR_LENGTH}`
|
|
6972
|
+
}
|
|
6973
|
+
}, 400);
|
|
6974
|
+
}
|
|
6975
|
+
}
|
|
6793
6976
|
const parsed = {
|
|
6794
6977
|
where: body.where,
|
|
6795
6978
|
orderBy: body.orderBy,
|
|
@@ -6798,8 +6981,9 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6798
6981
|
select: body.select,
|
|
6799
6982
|
include: body.include
|
|
6800
6983
|
};
|
|
6801
|
-
const
|
|
6802
|
-
const
|
|
6984
|
+
const evaluated = exposeEvalConfig ? await evaluateExposeDescriptors(exposeEvalConfig, entityCtx) : null;
|
|
6985
|
+
const relationsConfig = def.expose?.include ?? {};
|
|
6986
|
+
const validation = validateVertzQL(parsed, def.model.table, relationsConfig, exposeValidation, evaluated ?? undefined);
|
|
6803
6987
|
if (!validation.ok) {
|
|
6804
6988
|
return jsonResponse({ error: { code: "BadRequest", message: validation.error } }, 400);
|
|
6805
6989
|
}
|
|
@@ -6815,6 +6999,9 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6815
6999
|
const { status, body: errBody } = entityErrorHandler(result.error);
|
|
6816
7000
|
return jsonResponse(errBody, status);
|
|
6817
7001
|
}
|
|
7002
|
+
if (evaluated && evaluated.nulledFields.size > 0 && result.data.body.items) {
|
|
7003
|
+
result.data.body.items = result.data.body.items.map((row) => applyNulling(row, evaluated.nulledFields));
|
|
7004
|
+
}
|
|
6818
7005
|
if (parsed.select && result.data.body.items) {
|
|
6819
7006
|
result.data.body.items = result.data.body.items.map((row) => applySelect(parsed.select, row));
|
|
6820
7007
|
}
|
|
@@ -6848,8 +7035,9 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6848
7035
|
const id = getParams(ctx).id;
|
|
6849
7036
|
const query = ctx.query ?? {};
|
|
6850
7037
|
const parsed = parseVertzQL(query);
|
|
6851
|
-
const
|
|
6852
|
-
const
|
|
7038
|
+
const evaluated = exposeEvalConfig ? await evaluateExposeDescriptors(exposeEvalConfig, entityCtx) : null;
|
|
7039
|
+
const relationsConfig = def.expose?.include ?? {};
|
|
7040
|
+
const validation = validateVertzQL(parsed, def.model.table, relationsConfig, exposeValidation, evaluated ?? undefined);
|
|
6853
7041
|
if (!validation.ok) {
|
|
6854
7042
|
return jsonResponse({ error: { code: "BadRequest", message: validation.error } }, 400);
|
|
6855
7043
|
}
|
|
@@ -6859,7 +7047,11 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6859
7047
|
const { status, body: body2 } = entityErrorHandler(result.error);
|
|
6860
7048
|
return jsonResponse(body2, status);
|
|
6861
7049
|
}
|
|
6862
|
-
|
|
7050
|
+
let responseBody = result.data.body;
|
|
7051
|
+
if (evaluated && evaluated.nulledFields.size > 0) {
|
|
7052
|
+
responseBody = applyNulling(responseBody, evaluated.nulledFields);
|
|
7053
|
+
}
|
|
7054
|
+
const body = parsed.select ? applySelect(parsed.select, responseBody) : responseBody;
|
|
6863
7055
|
return jsonResponse(body, result.data.status);
|
|
6864
7056
|
} catch (error) {
|
|
6865
7057
|
const { status, body } = entityErrorHandler(error);
|
|
@@ -6894,7 +7086,14 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6894
7086
|
const { status, body } = entityErrorHandler(result.error);
|
|
6895
7087
|
return jsonResponse(body, status);
|
|
6896
7088
|
}
|
|
6897
|
-
|
|
7089
|
+
let responseBody = result.data.body;
|
|
7090
|
+
if (exposeEvalConfig) {
|
|
7091
|
+
const evaluated = await evaluateExposeDescriptors(exposeEvalConfig, entityCtx);
|
|
7092
|
+
if (evaluated.nulledFields.size > 0) {
|
|
7093
|
+
responseBody = applyNulling(responseBody, evaluated.nulledFields);
|
|
7094
|
+
}
|
|
7095
|
+
}
|
|
7096
|
+
return jsonResponse(responseBody, result.data.status);
|
|
6898
7097
|
} catch (error) {
|
|
6899
7098
|
const { status, body } = entityErrorHandler(error);
|
|
6900
7099
|
return jsonResponse(body, status);
|
|
@@ -6929,7 +7128,14 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6929
7128
|
const { status, body } = entityErrorHandler(result.error);
|
|
6930
7129
|
return jsonResponse(body, status);
|
|
6931
7130
|
}
|
|
6932
|
-
|
|
7131
|
+
let responseBody = result.data.body;
|
|
7132
|
+
if (exposeEvalConfig) {
|
|
7133
|
+
const evaluated = await evaluateExposeDescriptors(exposeEvalConfig, entityCtx);
|
|
7134
|
+
if (evaluated.nulledFields.size > 0) {
|
|
7135
|
+
responseBody = applyNulling(responseBody, evaluated.nulledFields);
|
|
7136
|
+
}
|
|
7137
|
+
}
|
|
7138
|
+
return jsonResponse(responseBody, result.data.status);
|
|
6933
7139
|
} catch (error) {
|
|
6934
7140
|
const { status, body } = entityErrorHandler(error);
|
|
6935
7141
|
return jsonResponse(body, status);
|
|
@@ -7447,7 +7653,7 @@ function entity(name, config) {
|
|
|
7447
7653
|
before: config.before ?? {},
|
|
7448
7654
|
after: config.after ?? {},
|
|
7449
7655
|
actions: config.actions ?? {},
|
|
7450
|
-
|
|
7656
|
+
expose: config.expose,
|
|
7451
7657
|
table: config.table ?? name,
|
|
7452
7658
|
tenantScoped,
|
|
7453
7659
|
tenantChain: null
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertz/server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Vertz server runtime — modules, routing, and auth",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"typecheck": "tsc --noEmit"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@vertz/core": "^0.2.
|
|
35
|
-
"@vertz/db": "^0.2.
|
|
36
|
-
"@vertz/errors": "^0.2.
|
|
37
|
-
"@vertz/schema": "^0.2.
|
|
34
|
+
"@vertz/core": "^0.2.17",
|
|
35
|
+
"@vertz/db": "^0.2.17",
|
|
36
|
+
"@vertz/errors": "^0.2.17",
|
|
37
|
+
"@vertz/schema": "^0.2.17",
|
|
38
38
|
"bcryptjs": "^3.0.3",
|
|
39
39
|
"jose": "^6.0.11"
|
|
40
40
|
},
|