@vertz/server 0.2.16 → 0.2.17
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 +40 -3
- package/dist/index.js +194 -21
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -1809,6 +1809,43 @@ interface RelationConfigObject<TColumnKeys extends string = string> {
|
|
|
1809
1809
|
readonly maxLimit?: number;
|
|
1810
1810
|
}
|
|
1811
1811
|
type EntityRelationsConfig<TRelations extends Record<string, RelationDef2> = Record<string, RelationDef2>> = { [K in keyof TRelations]? : true | false | RelationConfigObject<RelationColumnKeys<TRelations[K]>> };
|
|
1812
|
+
/** Extract the target table type from a RelationDef. */
|
|
1813
|
+
type RelationTargetTable<R> = R extends RelationDef2<infer T> ? T : TableDef;
|
|
1814
|
+
/** Structured expose config for a relation. Controls field, filter, sort, and nested relation exposure. */
|
|
1815
|
+
interface RelationExposeConfig<R extends RelationDef2 = RelationDef2> {
|
|
1816
|
+
/** Which fields of the related entity to expose. */
|
|
1817
|
+
readonly select: { [K in PublicColumnKeys<RelationTargetTable<R>>]? : true | AccessRule };
|
|
1818
|
+
/** Which fields clients can filter on via VertzQL `where`. */
|
|
1819
|
+
readonly allowWhere?: { [K in PublicColumnKeys<RelationTargetTable<R>>]? : true | AccessRule };
|
|
1820
|
+
/** Which fields clients can sort on via VertzQL `orderBy`. */
|
|
1821
|
+
readonly allowOrderBy?: { [K in PublicColumnKeys<RelationTargetTable<R>>]? : true | AccessRule };
|
|
1822
|
+
/** Max items returned per parent row. Defaults to DEFAULT_RELATION_LIMIT. */
|
|
1823
|
+
readonly maxLimit?: number;
|
|
1824
|
+
/** Nested relation exposure (loosely typed — target model relations not available from RelationDef). */
|
|
1825
|
+
readonly include?: Record<string, true | false | RelationExposeConfig>;
|
|
1826
|
+
}
|
|
1827
|
+
/** Controls which fields, filters, sorts, and relations are exposed through the entity API. */
|
|
1828
|
+
interface ExposeConfig<
|
|
1829
|
+
TTable extends TableDef = TableDef,
|
|
1830
|
+
TModel extends ModelDef4 = ModelDef4
|
|
1831
|
+
> {
|
|
1832
|
+
/** Which fields of the entity to expose. Required when expose is present. */
|
|
1833
|
+
readonly select: { [K in PublicColumnKeys<TTable>]? : true | AccessRule };
|
|
1834
|
+
/** Which fields clients can filter on via VertzQL `where`. */
|
|
1835
|
+
readonly allowWhere?: { [K in PublicColumnKeys<TTable>]? : true | AccessRule };
|
|
1836
|
+
/** Which fields clients can sort on via VertzQL `orderBy`. */
|
|
1837
|
+
readonly allowOrderBy?: { [K in PublicColumnKeys<TTable>]? : true | AccessRule };
|
|
1838
|
+
/** Which relations to expose and how. */
|
|
1839
|
+
readonly include?: { [K in keyof TModel["relations"]]? : true | false | RelationExposeConfig<TModel["relations"][K] extends RelationDef2 ? TModel["relations"][K] : RelationDef2> };
|
|
1840
|
+
}
|
|
1841
|
+
/** Extract non-hidden column keys from a table (public fields). */
|
|
1842
|
+
type PublicColumnKeys<TTable extends TableDef> = TTable extends TableDef<infer TCols> ? { [K in keyof TCols & string] : TCols[K] extends {
|
|
1843
|
+
_meta: {
|
|
1844
|
+
_annotations: {
|
|
1845
|
+
hidden: true;
|
|
1846
|
+
};
|
|
1847
|
+
};
|
|
1848
|
+
} ? never : K }[keyof TCols & string] : string;
|
|
1812
1849
|
interface EntityConfig<
|
|
1813
1850
|
TModel extends ModelDef4 = ModelDef4,
|
|
1814
1851
|
TActions extends Record<string, EntityActionDef<any, any, any, any>> = {},
|
|
@@ -1831,7 +1868,7 @@ interface EntityConfig<
|
|
|
1831
1868
|
readonly delete?: (row: TModel["table"]["$response"], ctx: EntityContext<TModel, TInject>) => void | Promise<void>;
|
|
1832
1869
|
};
|
|
1833
1870
|
readonly actions?: { readonly [K in keyof TActions] : TActions[K] };
|
|
1834
|
-
readonly
|
|
1871
|
+
readonly expose?: ExposeConfig<TModel["table"], TModel>;
|
|
1835
1872
|
}
|
|
1836
1873
|
interface EntityDefinition<TModel extends ModelDef4 = ModelDef4> {
|
|
1837
1874
|
readonly kind: "entity";
|
|
@@ -1842,7 +1879,7 @@ interface EntityDefinition<TModel extends ModelDef4 = ModelDef4> {
|
|
|
1842
1879
|
readonly before: EntityBeforeHooks;
|
|
1843
1880
|
readonly after: EntityAfterHooks;
|
|
1844
1881
|
readonly actions: Record<string, EntityActionDef>;
|
|
1845
|
-
readonly
|
|
1882
|
+
readonly expose?: ExposeConfig<TModel["table"], TModel>;
|
|
1846
1883
|
/** DB table name (defaults to entity name). */
|
|
1847
1884
|
readonly table: string;
|
|
1848
1885
|
/** Whether CRUD auto-filters by tenantId. */
|
|
@@ -2042,4 +2079,4 @@ declare function service<
|
|
|
2042
2079
|
TInject extends Record<string, EntityDefinition> = {},
|
|
2043
2080
|
TActions extends Record<string, ServiceActionDef<any, any, any>> = Record<string, ServiceActionDef<any, any, any>>
|
|
2044
2081
|
>(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 };
|
|
2082
|
+
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)) {
|
|
@@ -6241,6 +6250,10 @@ function createCrudHandlers(def, db, options) {
|
|
|
6241
6250
|
const tenantChain = options?.tenantChain ?? def.tenantChain ?? null;
|
|
6242
6251
|
const isIndirectlyScoped = tenantChain !== null;
|
|
6243
6252
|
const queryParentIds = options?.queryParentIds ?? null;
|
|
6253
|
+
const exposeSelect = def.expose?.select ? {
|
|
6254
|
+
...def.expose.select,
|
|
6255
|
+
...Object.fromEntries(Object.entries(def.expose.include ?? {}).filter(([, v]) => v !== false).map(([k]) => [k, true]))
|
|
6256
|
+
} : undefined;
|
|
6244
6257
|
function notFound(id) {
|
|
6245
6258
|
return err4(new EntityNotFoundError2(`${def.name} with id "${id}" not found`));
|
|
6246
6259
|
}
|
|
@@ -6311,7 +6324,7 @@ function createCrudHandlers(def, db, options) {
|
|
|
6311
6324
|
const orderBy = options2?.orderBy;
|
|
6312
6325
|
const include = options2?.include;
|
|
6313
6326
|
const { data: rows, total } = await db.list({ where, orderBy, limit, after, include });
|
|
6314
|
-
const data = rows.map((row) => narrowRelationFields(def.
|
|
6327
|
+
const data = rows.map((row) => applySelect(exposeSelect, narrowRelationFields(def.expose?.include ?? {}, stripHiddenFields(table, row))));
|
|
6315
6328
|
const pkColumn = resolvePrimaryKeyColumn(table);
|
|
6316
6329
|
const lastRow = rows[rows.length - 1];
|
|
6317
6330
|
const nextCursor = limit > 0 && rows.length === limit && lastRow ? String(lastRow[pkColumn]) : null;
|
|
@@ -6333,7 +6346,7 @@ function createCrudHandlers(def, db, options) {
|
|
|
6333
6346
|
return err4(accessResult.error);
|
|
6334
6347
|
return ok4({
|
|
6335
6348
|
status: 200,
|
|
6336
|
-
body: narrowRelationFields(def.
|
|
6349
|
+
body: applySelect(exposeSelect, narrowRelationFields(def.expose?.include ?? {}, stripHiddenFields(table, row)))
|
|
6337
6350
|
});
|
|
6338
6351
|
},
|
|
6339
6352
|
async create(ctx, data) {
|
|
@@ -6376,7 +6389,7 @@ function createCrudHandlers(def, db, options) {
|
|
|
6376
6389
|
}
|
|
6377
6390
|
return ok4({
|
|
6378
6391
|
status: 201,
|
|
6379
|
-
body: narrowRelationFields(def.
|
|
6392
|
+
body: applySelect(exposeSelect, narrowRelationFields(def.expose?.include ?? {}, strippedResult))
|
|
6380
6393
|
});
|
|
6381
6394
|
},
|
|
6382
6395
|
async update(ctx, id, data) {
|
|
@@ -6405,7 +6418,7 @@ function createCrudHandlers(def, db, options) {
|
|
|
6405
6418
|
}
|
|
6406
6419
|
return ok4({
|
|
6407
6420
|
status: 200,
|
|
6408
|
-
body: narrowRelationFields(def.
|
|
6421
|
+
body: applySelect(exposeSelect, narrowRelationFields(def.expose?.include ?? {}, strippedResult))
|
|
6409
6422
|
});
|
|
6410
6423
|
},
|
|
6411
6424
|
async delete(ctx, id) {
|
|
@@ -6493,7 +6506,101 @@ function entityErrorHandler(error) {
|
|
|
6493
6506
|
};
|
|
6494
6507
|
}
|
|
6495
6508
|
|
|
6509
|
+
// src/entity/expose-evaluator.ts
|
|
6510
|
+
async function evaluateExposeRule(rule, ctx, options) {
|
|
6511
|
+
switch (rule.type) {
|
|
6512
|
+
case "public":
|
|
6513
|
+
return true;
|
|
6514
|
+
case "authenticated":
|
|
6515
|
+
return ctx.authenticated();
|
|
6516
|
+
case "role":
|
|
6517
|
+
return ctx.role(...rule.roles);
|
|
6518
|
+
case "entitlement": {
|
|
6519
|
+
if (!options.can)
|
|
6520
|
+
return false;
|
|
6521
|
+
return options.can(rule.entitlement);
|
|
6522
|
+
}
|
|
6523
|
+
case "where":
|
|
6524
|
+
return false;
|
|
6525
|
+
case "all": {
|
|
6526
|
+
for (const sub of rule.rules) {
|
|
6527
|
+
if (!await evaluateExposeRule(sub, ctx, options))
|
|
6528
|
+
return false;
|
|
6529
|
+
}
|
|
6530
|
+
return true;
|
|
6531
|
+
}
|
|
6532
|
+
case "any": {
|
|
6533
|
+
for (const sub of rule.rules) {
|
|
6534
|
+
if (await evaluateExposeRule(sub, ctx, options))
|
|
6535
|
+
return true;
|
|
6536
|
+
}
|
|
6537
|
+
return false;
|
|
6538
|
+
}
|
|
6539
|
+
case "fva": {
|
|
6540
|
+
if (options.fvaAge === undefined)
|
|
6541
|
+
return false;
|
|
6542
|
+
return options.fvaAge <= rule.maxAge;
|
|
6543
|
+
}
|
|
6544
|
+
}
|
|
6545
|
+
}
|
|
6546
|
+
async function evaluateExposeDescriptors(expose, ctx, options = {}) {
|
|
6547
|
+
const allowedSelectFields = new Set;
|
|
6548
|
+
const nulledFields = new Set;
|
|
6549
|
+
const allowedWhereFields = new Set;
|
|
6550
|
+
const allowedOrderByFields = new Set;
|
|
6551
|
+
for (const [field, value] of Object.entries(expose.select)) {
|
|
6552
|
+
if (value === true) {
|
|
6553
|
+
allowedSelectFields.add(field);
|
|
6554
|
+
} else {
|
|
6555
|
+
const passed = await evaluateExposeRule(value, ctx, options);
|
|
6556
|
+
if (passed) {
|
|
6557
|
+
allowedSelectFields.add(field);
|
|
6558
|
+
} else {
|
|
6559
|
+
allowedSelectFields.add(field);
|
|
6560
|
+
nulledFields.add(field);
|
|
6561
|
+
}
|
|
6562
|
+
}
|
|
6563
|
+
}
|
|
6564
|
+
if (expose.allowWhere) {
|
|
6565
|
+
for (const [field, value] of Object.entries(expose.allowWhere)) {
|
|
6566
|
+
if (value === true) {
|
|
6567
|
+
allowedWhereFields.add(field);
|
|
6568
|
+
} else {
|
|
6569
|
+
const passed = await evaluateExposeRule(value, ctx, options);
|
|
6570
|
+
if (passed) {
|
|
6571
|
+
allowedWhereFields.add(field);
|
|
6572
|
+
}
|
|
6573
|
+
}
|
|
6574
|
+
}
|
|
6575
|
+
}
|
|
6576
|
+
if (expose.allowOrderBy) {
|
|
6577
|
+
for (const [field, value] of Object.entries(expose.allowOrderBy)) {
|
|
6578
|
+
if (value === true) {
|
|
6579
|
+
allowedOrderByFields.add(field);
|
|
6580
|
+
} else {
|
|
6581
|
+
const passed = await evaluateExposeRule(value, ctx, options);
|
|
6582
|
+
if (passed) {
|
|
6583
|
+
allowedOrderByFields.add(field);
|
|
6584
|
+
}
|
|
6585
|
+
}
|
|
6586
|
+
}
|
|
6587
|
+
}
|
|
6588
|
+
return {
|
|
6589
|
+
allowedSelectFields,
|
|
6590
|
+
nulledFields,
|
|
6591
|
+
allowedWhereFields,
|
|
6592
|
+
allowedOrderByFields
|
|
6593
|
+
};
|
|
6594
|
+
}
|
|
6595
|
+
|
|
6496
6596
|
// src/entity/vertzql-parser.ts
|
|
6597
|
+
function extractAllowKeys(allow) {
|
|
6598
|
+
if (!allow)
|
|
6599
|
+
return [];
|
|
6600
|
+
if (Array.isArray(allow))
|
|
6601
|
+
return allow;
|
|
6602
|
+
return Object.keys(allow);
|
|
6603
|
+
}
|
|
6497
6604
|
var MAX_LIMIT = 1000;
|
|
6498
6605
|
var MAX_Q_BASE64_LENGTH = 10240;
|
|
6499
6606
|
var ALLOWED_Q_KEYS = new Set(["select", "include", "where", "orderBy", "limit", "offset"]);
|
|
@@ -6579,30 +6686,50 @@ function getHiddenColumns(table) {
|
|
|
6579
6686
|
}
|
|
6580
6687
|
return hidden;
|
|
6581
6688
|
}
|
|
6582
|
-
function validateVertzQL(options, table, relationsConfig) {
|
|
6689
|
+
function validateVertzQL(options, table, relationsConfig, exposeConfig, evaluatedExpose) {
|
|
6583
6690
|
if (options._qError) {
|
|
6584
6691
|
return { ok: false, error: options._qError };
|
|
6585
6692
|
}
|
|
6586
6693
|
const hiddenColumns = getHiddenColumns(table);
|
|
6587
6694
|
if (options.where) {
|
|
6695
|
+
const allowWhereSet = evaluatedExpose ? evaluatedExpose.allowedWhereFields : null;
|
|
6696
|
+
const allowWhereKeys = !evaluatedExpose && exposeConfig ? extractAllowKeys(exposeConfig.allowWhere) : null;
|
|
6588
6697
|
for (const field of Object.keys(options.where)) {
|
|
6589
6698
|
if (hiddenColumns.has(field)) {
|
|
6590
6699
|
return { ok: false, error: `Field "${field}" is not filterable` };
|
|
6591
6700
|
}
|
|
6701
|
+
if (allowWhereSet !== null && !allowWhereSet.has(field)) {
|
|
6702
|
+
return { ok: false, error: `Field "${field}" is not filterable` };
|
|
6703
|
+
}
|
|
6704
|
+
if (allowWhereKeys !== null && !allowWhereKeys.includes(field)) {
|
|
6705
|
+
return { ok: false, error: `Field "${field}" is not filterable` };
|
|
6706
|
+
}
|
|
6592
6707
|
}
|
|
6593
6708
|
}
|
|
6594
6709
|
if (options.orderBy) {
|
|
6710
|
+
const allowOrderBySet = evaluatedExpose ? evaluatedExpose.allowedOrderByFields : null;
|
|
6711
|
+
const allowOrderByKeys = !evaluatedExpose && exposeConfig ? extractAllowKeys(exposeConfig.allowOrderBy) : null;
|
|
6595
6712
|
for (const field of Object.keys(options.orderBy)) {
|
|
6596
6713
|
if (hiddenColumns.has(field)) {
|
|
6597
6714
|
return { ok: false, error: `Field "${field}" is not sortable` };
|
|
6598
6715
|
}
|
|
6716
|
+
if (allowOrderBySet !== null && !allowOrderBySet.has(field)) {
|
|
6717
|
+
return { ok: false, error: `Field "${field}" is not sortable` };
|
|
6718
|
+
}
|
|
6719
|
+
if (allowOrderByKeys !== null && !allowOrderByKeys.includes(field)) {
|
|
6720
|
+
return { ok: false, error: `Field "${field}" is not sortable` };
|
|
6721
|
+
}
|
|
6599
6722
|
}
|
|
6600
6723
|
}
|
|
6601
6724
|
if (options.select) {
|
|
6725
|
+
const exposeSelectKeys = exposeConfig ? extractAllowKeys(exposeConfig.select) : null;
|
|
6602
6726
|
for (const field of Object.keys(options.select)) {
|
|
6603
6727
|
if (hiddenColumns.has(field)) {
|
|
6604
6728
|
return { ok: false, error: `Field "${field}" is not selectable` };
|
|
6605
6729
|
}
|
|
6730
|
+
if (exposeSelectKeys !== null && exposeSelectKeys.length > 0 && !exposeSelectKeys.includes(field)) {
|
|
6731
|
+
return { ok: false, error: `Field "${field}" is not selectable` };
|
|
6732
|
+
}
|
|
6606
6733
|
}
|
|
6607
6734
|
}
|
|
6608
6735
|
if (options.include && relationsConfig) {
|
|
@@ -6623,35 +6750,37 @@ function validateInclude(include, relationsConfig, pathPrefix) {
|
|
|
6623
6750
|
continue;
|
|
6624
6751
|
const configObj = typeof entityConfig === "object" ? entityConfig : undefined;
|
|
6625
6752
|
if (requested.where) {
|
|
6626
|
-
|
|
6753
|
+
const allowWhereKeys = extractAllowKeys(configObj?.allowWhere);
|
|
6754
|
+
if (!configObj || allowWhereKeys.length === 0) {
|
|
6627
6755
|
return {
|
|
6628
6756
|
ok: false,
|
|
6629
6757
|
error: `Filtering is not enabled on relation '${relationPath}'. ` + "Add 'allowWhere' to the entity relations config."
|
|
6630
6758
|
};
|
|
6631
6759
|
}
|
|
6632
|
-
const allowedSet = new Set(
|
|
6760
|
+
const allowedSet = new Set(allowWhereKeys);
|
|
6633
6761
|
for (const field of Object.keys(requested.where)) {
|
|
6634
6762
|
if (!allowedSet.has(field)) {
|
|
6635
6763
|
return {
|
|
6636
6764
|
ok: false,
|
|
6637
|
-
error: `Field '${field}' is not filterable on relation '${relationPath}'. ` + `Allowed: ${
|
|
6765
|
+
error: `Field '${field}' is not filterable on relation '${relationPath}'. ` + `Allowed: ${allowWhereKeys.join(", ")}`
|
|
6638
6766
|
};
|
|
6639
6767
|
}
|
|
6640
6768
|
}
|
|
6641
6769
|
}
|
|
6642
6770
|
if (requested.orderBy) {
|
|
6643
|
-
|
|
6771
|
+
const allowOrderByKeys = extractAllowKeys(configObj?.allowOrderBy);
|
|
6772
|
+
if (!configObj || allowOrderByKeys.length === 0) {
|
|
6644
6773
|
return {
|
|
6645
6774
|
ok: false,
|
|
6646
6775
|
error: `Sorting is not enabled on relation '${relationPath}'. ` + "Add 'allowOrderBy' to the entity relations config."
|
|
6647
6776
|
};
|
|
6648
6777
|
}
|
|
6649
|
-
const allowedSet = new Set(
|
|
6778
|
+
const allowedSet = new Set(allowOrderByKeys);
|
|
6650
6779
|
for (const [field, dir] of Object.entries(requested.orderBy)) {
|
|
6651
6780
|
if (!allowedSet.has(field)) {
|
|
6652
6781
|
return {
|
|
6653
6782
|
ok: false,
|
|
6654
|
-
error: `Field '${field}' is not sortable on relation '${relationPath}'. ` + `Allowed: ${
|
|
6783
|
+
error: `Field '${field}' is not sortable on relation '${relationPath}'. ` + `Allowed: ${allowOrderByKeys.join(", ")}`
|
|
6655
6784
|
};
|
|
6656
6785
|
}
|
|
6657
6786
|
if (dir !== "asc" && dir !== "desc") {
|
|
@@ -6718,10 +6847,27 @@ function extractRequestInfo(ctx) {
|
|
|
6718
6847
|
function getParams(ctx) {
|
|
6719
6848
|
return ctx.params ?? {};
|
|
6720
6849
|
}
|
|
6850
|
+
function hasDescriptorValues(record) {
|
|
6851
|
+
if (!record)
|
|
6852
|
+
return false;
|
|
6853
|
+
return Object.values(record).some((v) => v !== true);
|
|
6854
|
+
}
|
|
6855
|
+
function hasDescriptors(expose) {
|
|
6856
|
+
return hasDescriptorValues(expose.select) || hasDescriptorValues(expose.allowWhere) || hasDescriptorValues(expose.allowOrderBy);
|
|
6857
|
+
}
|
|
6858
|
+
function applyNulling(data, nulledFields) {
|
|
6859
|
+
return nullGuardedFields(nulledFields, data);
|
|
6860
|
+
}
|
|
6721
6861
|
function generateEntityRoutes(def, registry, db, options) {
|
|
6722
6862
|
const prefix = options?.apiPrefix ?? "/api";
|
|
6723
6863
|
const basePath = `${prefix}/${def.name}`;
|
|
6724
6864
|
const tenantChain = options?.tenantChain ?? null;
|
|
6865
|
+
const exposeValidation = def.expose ? {
|
|
6866
|
+
select: def.expose.select,
|
|
6867
|
+
allowWhere: def.expose.allowWhere,
|
|
6868
|
+
allowOrderBy: def.expose.allowOrderBy
|
|
6869
|
+
} : undefined;
|
|
6870
|
+
const exposeEvalConfig = def.expose ? hasDescriptors(def.expose) ? def.expose : null : null;
|
|
6725
6871
|
const crudHandlers = createCrudHandlers(def, db, {
|
|
6726
6872
|
tenantChain,
|
|
6727
6873
|
queryParentIds: options?.queryParentIds
|
|
@@ -6755,8 +6901,9 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6755
6901
|
const entityCtx = makeEntityCtx(ctx);
|
|
6756
6902
|
const query = ctx.query ?? {};
|
|
6757
6903
|
const parsed = parseVertzQL(query);
|
|
6758
|
-
const
|
|
6759
|
-
const
|
|
6904
|
+
const evaluated = exposeEvalConfig ? await evaluateExposeDescriptors(exposeEvalConfig, entityCtx) : null;
|
|
6905
|
+
const relationsConfig = def.expose?.include ?? {};
|
|
6906
|
+
const validation = validateVertzQL(parsed, def.model.table, relationsConfig, exposeValidation, evaluated ?? undefined);
|
|
6760
6907
|
if (!validation.ok) {
|
|
6761
6908
|
return jsonResponse({ error: { code: "BadRequest", message: validation.error } }, 400);
|
|
6762
6909
|
}
|
|
@@ -6772,6 +6919,9 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6772
6919
|
const { status, body } = entityErrorHandler(result.error);
|
|
6773
6920
|
return jsonResponse(body, status);
|
|
6774
6921
|
}
|
|
6922
|
+
if (evaluated && evaluated.nulledFields.size > 0 && result.data.body.items) {
|
|
6923
|
+
result.data.body.items = result.data.body.items.map((row) => applyNulling(row, evaluated.nulledFields));
|
|
6924
|
+
}
|
|
6775
6925
|
if (parsed.select && result.data.body.items) {
|
|
6776
6926
|
result.data.body.items = result.data.body.items.map((row) => applySelect(parsed.select, row));
|
|
6777
6927
|
}
|
|
@@ -6798,8 +6948,9 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6798
6948
|
select: body.select,
|
|
6799
6949
|
include: body.include
|
|
6800
6950
|
};
|
|
6801
|
-
const
|
|
6802
|
-
const
|
|
6951
|
+
const evaluated = exposeEvalConfig ? await evaluateExposeDescriptors(exposeEvalConfig, entityCtx) : null;
|
|
6952
|
+
const relationsConfig = def.expose?.include ?? {};
|
|
6953
|
+
const validation = validateVertzQL(parsed, def.model.table, relationsConfig, exposeValidation, evaluated ?? undefined);
|
|
6803
6954
|
if (!validation.ok) {
|
|
6804
6955
|
return jsonResponse({ error: { code: "BadRequest", message: validation.error } }, 400);
|
|
6805
6956
|
}
|
|
@@ -6815,6 +6966,9 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6815
6966
|
const { status, body: errBody } = entityErrorHandler(result.error);
|
|
6816
6967
|
return jsonResponse(errBody, status);
|
|
6817
6968
|
}
|
|
6969
|
+
if (evaluated && evaluated.nulledFields.size > 0 && result.data.body.items) {
|
|
6970
|
+
result.data.body.items = result.data.body.items.map((row) => applyNulling(row, evaluated.nulledFields));
|
|
6971
|
+
}
|
|
6818
6972
|
if (parsed.select && result.data.body.items) {
|
|
6819
6973
|
result.data.body.items = result.data.body.items.map((row) => applySelect(parsed.select, row));
|
|
6820
6974
|
}
|
|
@@ -6848,8 +7002,9 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6848
7002
|
const id = getParams(ctx).id;
|
|
6849
7003
|
const query = ctx.query ?? {};
|
|
6850
7004
|
const parsed = parseVertzQL(query);
|
|
6851
|
-
const
|
|
6852
|
-
const
|
|
7005
|
+
const evaluated = exposeEvalConfig ? await evaluateExposeDescriptors(exposeEvalConfig, entityCtx) : null;
|
|
7006
|
+
const relationsConfig = def.expose?.include ?? {};
|
|
7007
|
+
const validation = validateVertzQL(parsed, def.model.table, relationsConfig, exposeValidation, evaluated ?? undefined);
|
|
6853
7008
|
if (!validation.ok) {
|
|
6854
7009
|
return jsonResponse({ error: { code: "BadRequest", message: validation.error } }, 400);
|
|
6855
7010
|
}
|
|
@@ -6859,7 +7014,11 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6859
7014
|
const { status, body: body2 } = entityErrorHandler(result.error);
|
|
6860
7015
|
return jsonResponse(body2, status);
|
|
6861
7016
|
}
|
|
6862
|
-
|
|
7017
|
+
let responseBody = result.data.body;
|
|
7018
|
+
if (evaluated && evaluated.nulledFields.size > 0) {
|
|
7019
|
+
responseBody = applyNulling(responseBody, evaluated.nulledFields);
|
|
7020
|
+
}
|
|
7021
|
+
const body = parsed.select ? applySelect(parsed.select, responseBody) : responseBody;
|
|
6863
7022
|
return jsonResponse(body, result.data.status);
|
|
6864
7023
|
} catch (error) {
|
|
6865
7024
|
const { status, body } = entityErrorHandler(error);
|
|
@@ -6894,7 +7053,14 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6894
7053
|
const { status, body } = entityErrorHandler(result.error);
|
|
6895
7054
|
return jsonResponse(body, status);
|
|
6896
7055
|
}
|
|
6897
|
-
|
|
7056
|
+
let responseBody = result.data.body;
|
|
7057
|
+
if (exposeEvalConfig) {
|
|
7058
|
+
const evaluated = await evaluateExposeDescriptors(exposeEvalConfig, entityCtx);
|
|
7059
|
+
if (evaluated.nulledFields.size > 0) {
|
|
7060
|
+
responseBody = applyNulling(responseBody, evaluated.nulledFields);
|
|
7061
|
+
}
|
|
7062
|
+
}
|
|
7063
|
+
return jsonResponse(responseBody, result.data.status);
|
|
6898
7064
|
} catch (error) {
|
|
6899
7065
|
const { status, body } = entityErrorHandler(error);
|
|
6900
7066
|
return jsonResponse(body, status);
|
|
@@ -6929,7 +7095,14 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6929
7095
|
const { status, body } = entityErrorHandler(result.error);
|
|
6930
7096
|
return jsonResponse(body, status);
|
|
6931
7097
|
}
|
|
6932
|
-
|
|
7098
|
+
let responseBody = result.data.body;
|
|
7099
|
+
if (exposeEvalConfig) {
|
|
7100
|
+
const evaluated = await evaluateExposeDescriptors(exposeEvalConfig, entityCtx);
|
|
7101
|
+
if (evaluated.nulledFields.size > 0) {
|
|
7102
|
+
responseBody = applyNulling(responseBody, evaluated.nulledFields);
|
|
7103
|
+
}
|
|
7104
|
+
}
|
|
7105
|
+
return jsonResponse(responseBody, result.data.status);
|
|
6933
7106
|
} catch (error) {
|
|
6934
7107
|
const { status, body } = entityErrorHandler(error);
|
|
6935
7108
|
return jsonResponse(body, status);
|
|
@@ -7447,7 +7620,7 @@ function entity(name, config) {
|
|
|
7447
7620
|
before: config.before ?? {},
|
|
7448
7621
|
after: config.after ?? {},
|
|
7449
7622
|
actions: config.actions ?? {},
|
|
7450
|
-
|
|
7623
|
+
expose: config.expose,
|
|
7451
7624
|
table: config.table ?? name,
|
|
7452
7625
|
tenantScoped,
|
|
7453
7626
|
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.17",
|
|
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.16",
|
|
35
|
+
"@vertz/db": "^0.2.16",
|
|
36
|
+
"@vertz/errors": "^0.2.16",
|
|
37
|
+
"@vertz/schema": "^0.2.16",
|
|
38
38
|
"bcryptjs": "^3.0.3",
|
|
39
39
|
"jose": "^6.0.11"
|
|
40
40
|
},
|