@vertz/server 0.2.17 → 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 +136 -132
- package/dist/index.js +268 -235
- 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 {
|
package/dist/index.js
CHANGED
|
@@ -6236,6 +6236,260 @@ import {
|
|
|
6236
6236
|
err as err4,
|
|
6237
6237
|
ok as ok4
|
|
6238
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
|
|
6239
6493
|
function resolvePrimaryKeyColumn(table) {
|
|
6240
6494
|
for (const key of Object.keys(table._columns)) {
|
|
6241
6495
|
const col = table._columns[key];
|
|
@@ -6320,7 +6574,7 @@ function createCrudHandlers(def, db, options) {
|
|
|
6320
6574
|
const indirectWhere = await resolveIndirectTenantWhere(ctx);
|
|
6321
6575
|
const where = indirectWhere ? { ...directWhere, ...indirectWhere } : directWhere;
|
|
6322
6576
|
const limit = Math.max(0, options2?.limit ?? 20);
|
|
6323
|
-
const after = options2?.after && options2.after.length <=
|
|
6577
|
+
const after = options2?.after && options2.after.length <= MAX_CURSOR_LENGTH ? options2.after : undefined;
|
|
6324
6578
|
const orderBy = options2?.orderBy;
|
|
6325
6579
|
const include = options2?.include;
|
|
6326
6580
|
const { data: rows, total } = await db.list({ where, orderBy, limit, after, include });
|
|
@@ -6593,240 +6847,6 @@ async function evaluateExposeDescriptors(expose, ctx, options = {}) {
|
|
|
6593
6847
|
};
|
|
6594
6848
|
}
|
|
6595
6849
|
|
|
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
|
-
}
|
|
6604
|
-
var MAX_LIMIT = 1000;
|
|
6605
|
-
var MAX_Q_BASE64_LENGTH = 10240;
|
|
6606
|
-
var ALLOWED_Q_KEYS = new Set(["select", "include", "where", "orderBy", "limit", "offset"]);
|
|
6607
|
-
function parseVertzQL(query) {
|
|
6608
|
-
const result = {};
|
|
6609
|
-
for (const [key, value] of Object.entries(query)) {
|
|
6610
|
-
const whereMatch = key.match(/^where\[([^\]]+)\](?:\[([^\]]+)\])?$/);
|
|
6611
|
-
if (whereMatch) {
|
|
6612
|
-
if (!result.where)
|
|
6613
|
-
result.where = {};
|
|
6614
|
-
const field = whereMatch[1];
|
|
6615
|
-
const op = whereMatch[2];
|
|
6616
|
-
const existing = result.where[field];
|
|
6617
|
-
if (op) {
|
|
6618
|
-
const base = existing && typeof existing === "object" ? existing : existing !== undefined ? { eq: existing } : {};
|
|
6619
|
-
result.where[field] = { ...base, [op]: value };
|
|
6620
|
-
} else {
|
|
6621
|
-
if (existing && typeof existing === "object") {
|
|
6622
|
-
result.where[field] = { ...existing, eq: value };
|
|
6623
|
-
} else {
|
|
6624
|
-
result.where[field] = value;
|
|
6625
|
-
}
|
|
6626
|
-
}
|
|
6627
|
-
continue;
|
|
6628
|
-
}
|
|
6629
|
-
if (key === "limit") {
|
|
6630
|
-
const parsed = Number.parseInt(value, 10);
|
|
6631
|
-
if (!Number.isNaN(parsed)) {
|
|
6632
|
-
result.limit = Math.max(0, Math.min(parsed, MAX_LIMIT));
|
|
6633
|
-
}
|
|
6634
|
-
continue;
|
|
6635
|
-
}
|
|
6636
|
-
if (key === "after") {
|
|
6637
|
-
if (value) {
|
|
6638
|
-
result.after = value;
|
|
6639
|
-
}
|
|
6640
|
-
continue;
|
|
6641
|
-
}
|
|
6642
|
-
if (key === "orderBy") {
|
|
6643
|
-
const [field, dir] = value.split(":");
|
|
6644
|
-
if (field) {
|
|
6645
|
-
if (!result.orderBy)
|
|
6646
|
-
result.orderBy = {};
|
|
6647
|
-
result.orderBy[field] = dir === "desc" ? "desc" : "asc";
|
|
6648
|
-
}
|
|
6649
|
-
continue;
|
|
6650
|
-
}
|
|
6651
|
-
if (key === "q") {
|
|
6652
|
-
try {
|
|
6653
|
-
const urlDecoded = decodeURIComponent(value);
|
|
6654
|
-
if (urlDecoded.length > MAX_Q_BASE64_LENGTH) {
|
|
6655
|
-
result._qError = "q= parameter exceeds maximum allowed size";
|
|
6656
|
-
continue;
|
|
6657
|
-
}
|
|
6658
|
-
const b64 = urlDecoded.replace(/-/g, "+").replace(/_/g, "/");
|
|
6659
|
-
const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
|
|
6660
|
-
const decoded = JSON.parse(atob(padded));
|
|
6661
|
-
for (const k of Object.keys(decoded)) {
|
|
6662
|
-
if (!ALLOWED_Q_KEYS.has(k)) {
|
|
6663
|
-
delete decoded[k];
|
|
6664
|
-
}
|
|
6665
|
-
}
|
|
6666
|
-
if (decoded.select && typeof decoded.select === "object") {
|
|
6667
|
-
result.select = decoded.select;
|
|
6668
|
-
}
|
|
6669
|
-
if (decoded.include && typeof decoded.include === "object") {
|
|
6670
|
-
result.include = decoded.include;
|
|
6671
|
-
}
|
|
6672
|
-
} catch {
|
|
6673
|
-
result._qError = "Invalid q= parameter: not valid base64 or JSON";
|
|
6674
|
-
}
|
|
6675
|
-
}
|
|
6676
|
-
}
|
|
6677
|
-
return result;
|
|
6678
|
-
}
|
|
6679
|
-
function getHiddenColumns(table) {
|
|
6680
|
-
const hidden = new Set;
|
|
6681
|
-
for (const key of Object.keys(table._columns)) {
|
|
6682
|
-
const col = table._columns[key];
|
|
6683
|
-
if (col?._meta._annotations.hidden) {
|
|
6684
|
-
hidden.add(key);
|
|
6685
|
-
}
|
|
6686
|
-
}
|
|
6687
|
-
return hidden;
|
|
6688
|
-
}
|
|
6689
|
-
function validateVertzQL(options, table, relationsConfig, exposeConfig, evaluatedExpose) {
|
|
6690
|
-
if (options._qError) {
|
|
6691
|
-
return { ok: false, error: options._qError };
|
|
6692
|
-
}
|
|
6693
|
-
const hiddenColumns = getHiddenColumns(table);
|
|
6694
|
-
if (options.where) {
|
|
6695
|
-
const allowWhereSet = evaluatedExpose ? evaluatedExpose.allowedWhereFields : null;
|
|
6696
|
-
const allowWhereKeys = !evaluatedExpose && exposeConfig ? extractAllowKeys(exposeConfig.allowWhere) : null;
|
|
6697
|
-
for (const field of Object.keys(options.where)) {
|
|
6698
|
-
if (hiddenColumns.has(field)) {
|
|
6699
|
-
return { ok: false, error: `Field "${field}" is not filterable` };
|
|
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
|
-
}
|
|
6707
|
-
}
|
|
6708
|
-
}
|
|
6709
|
-
if (options.orderBy) {
|
|
6710
|
-
const allowOrderBySet = evaluatedExpose ? evaluatedExpose.allowedOrderByFields : null;
|
|
6711
|
-
const allowOrderByKeys = !evaluatedExpose && exposeConfig ? extractAllowKeys(exposeConfig.allowOrderBy) : null;
|
|
6712
|
-
for (const field of Object.keys(options.orderBy)) {
|
|
6713
|
-
if (hiddenColumns.has(field)) {
|
|
6714
|
-
return { ok: false, error: `Field "${field}" is not sortable` };
|
|
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
|
-
}
|
|
6722
|
-
}
|
|
6723
|
-
}
|
|
6724
|
-
if (options.select) {
|
|
6725
|
-
const exposeSelectKeys = exposeConfig ? extractAllowKeys(exposeConfig.select) : null;
|
|
6726
|
-
for (const field of Object.keys(options.select)) {
|
|
6727
|
-
if (hiddenColumns.has(field)) {
|
|
6728
|
-
return { ok: false, error: `Field "${field}" is not selectable` };
|
|
6729
|
-
}
|
|
6730
|
-
if (exposeSelectKeys !== null && exposeSelectKeys.length > 0 && !exposeSelectKeys.includes(field)) {
|
|
6731
|
-
return { ok: false, error: `Field "${field}" is not selectable` };
|
|
6732
|
-
}
|
|
6733
|
-
}
|
|
6734
|
-
}
|
|
6735
|
-
if (options.include && relationsConfig) {
|
|
6736
|
-
const includeResult = validateInclude(options.include, relationsConfig, "");
|
|
6737
|
-
if (!includeResult.ok)
|
|
6738
|
-
return includeResult;
|
|
6739
|
-
}
|
|
6740
|
-
return { ok: true };
|
|
6741
|
-
}
|
|
6742
|
-
function validateInclude(include, relationsConfig, pathPrefix) {
|
|
6743
|
-
for (const [relation, requested] of Object.entries(include)) {
|
|
6744
|
-
const entityConfig = relationsConfig[relation];
|
|
6745
|
-
const relationPath = pathPrefix ? `${pathPrefix}.${relation}` : relation;
|
|
6746
|
-
if (entityConfig === undefined || entityConfig === false) {
|
|
6747
|
-
return { ok: false, error: `Relation "${relationPath}" is not exposed` };
|
|
6748
|
-
}
|
|
6749
|
-
if (requested === true)
|
|
6750
|
-
continue;
|
|
6751
|
-
const configObj = typeof entityConfig === "object" ? entityConfig : undefined;
|
|
6752
|
-
if (requested.where) {
|
|
6753
|
-
const allowWhereKeys = extractAllowKeys(configObj?.allowWhere);
|
|
6754
|
-
if (!configObj || allowWhereKeys.length === 0) {
|
|
6755
|
-
return {
|
|
6756
|
-
ok: false,
|
|
6757
|
-
error: `Filtering is not enabled on relation '${relationPath}'. ` + "Add 'allowWhere' to the entity relations config."
|
|
6758
|
-
};
|
|
6759
|
-
}
|
|
6760
|
-
const allowedSet = new Set(allowWhereKeys);
|
|
6761
|
-
for (const field of Object.keys(requested.where)) {
|
|
6762
|
-
if (!allowedSet.has(field)) {
|
|
6763
|
-
return {
|
|
6764
|
-
ok: false,
|
|
6765
|
-
error: `Field '${field}' is not filterable on relation '${relationPath}'. ` + `Allowed: ${allowWhereKeys.join(", ")}`
|
|
6766
|
-
};
|
|
6767
|
-
}
|
|
6768
|
-
}
|
|
6769
|
-
}
|
|
6770
|
-
if (requested.orderBy) {
|
|
6771
|
-
const allowOrderByKeys = extractAllowKeys(configObj?.allowOrderBy);
|
|
6772
|
-
if (!configObj || allowOrderByKeys.length === 0) {
|
|
6773
|
-
return {
|
|
6774
|
-
ok: false,
|
|
6775
|
-
error: `Sorting is not enabled on relation '${relationPath}'. ` + "Add 'allowOrderBy' to the entity relations config."
|
|
6776
|
-
};
|
|
6777
|
-
}
|
|
6778
|
-
const allowedSet = new Set(allowOrderByKeys);
|
|
6779
|
-
for (const [field, dir] of Object.entries(requested.orderBy)) {
|
|
6780
|
-
if (!allowedSet.has(field)) {
|
|
6781
|
-
return {
|
|
6782
|
-
ok: false,
|
|
6783
|
-
error: `Field '${field}' is not sortable on relation '${relationPath}'. ` + `Allowed: ${allowOrderByKeys.join(", ")}`
|
|
6784
|
-
};
|
|
6785
|
-
}
|
|
6786
|
-
if (dir !== "asc" && dir !== "desc") {
|
|
6787
|
-
return {
|
|
6788
|
-
ok: false,
|
|
6789
|
-
error: `Invalid orderBy direction '${String(dir)}' for field '${field}' on relation '${relationPath}'. Must be 'asc' or 'desc'.`
|
|
6790
|
-
};
|
|
6791
|
-
}
|
|
6792
|
-
}
|
|
6793
|
-
}
|
|
6794
|
-
if (requested.limit !== undefined) {
|
|
6795
|
-
if (typeof requested.limit !== "number" || !Number.isFinite(requested.limit)) {
|
|
6796
|
-
return {
|
|
6797
|
-
ok: false,
|
|
6798
|
-
error: `Invalid limit on relation '${relationPath}': must be a finite number`
|
|
6799
|
-
};
|
|
6800
|
-
}
|
|
6801
|
-
if (requested.limit < 0) {
|
|
6802
|
-
requested.limit = 0;
|
|
6803
|
-
}
|
|
6804
|
-
if (configObj?.maxLimit !== undefined && requested.limit > configObj.maxLimit) {
|
|
6805
|
-
requested.limit = configObj.maxLimit;
|
|
6806
|
-
}
|
|
6807
|
-
}
|
|
6808
|
-
if (requested.select && configObj?.select) {
|
|
6809
|
-
for (const field of Object.keys(requested.select)) {
|
|
6810
|
-
if (!(field in configObj.select)) {
|
|
6811
|
-
return {
|
|
6812
|
-
ok: false,
|
|
6813
|
-
error: `Field "${field}" is not exposed on relation "${relationPath}"`
|
|
6814
|
-
};
|
|
6815
|
-
}
|
|
6816
|
-
}
|
|
6817
|
-
}
|
|
6818
|
-
if (requested.include) {
|
|
6819
|
-
if (entityConfig === true) {
|
|
6820
|
-
return {
|
|
6821
|
-
ok: false,
|
|
6822
|
-
error: `Nested includes are not supported on relation '${relationPath}' ` + "without a structured relations config."
|
|
6823
|
-
};
|
|
6824
|
-
}
|
|
6825
|
-
}
|
|
6826
|
-
}
|
|
6827
|
-
return { ok: true };
|
|
6828
|
-
}
|
|
6829
|
-
|
|
6830
6850
|
// src/entity/route-generator.ts
|
|
6831
6851
|
function jsonResponse(data, status = 200) {
|
|
6832
6852
|
return new Response(JSON.stringify(data), {
|
|
@@ -6940,6 +6960,19 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6940
6960
|
try {
|
|
6941
6961
|
const entityCtx = makeEntityCtx(ctx);
|
|
6942
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
|
+
}
|
|
6943
6976
|
const parsed = {
|
|
6944
6977
|
where: body.where,
|
|
6945
6978
|
orderBy: body.orderBy,
|
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
|
},
|