@veloxts/router 0.6.97 → 0.6.99
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/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/procedure/builder.js +45 -16
- package/dist/procedure/types.d.ts +22 -23
- package/dist/resource/index.d.ts +3 -3
- package/dist/resource/index.js +1 -1
- package/dist/resource/instance.d.ts +29 -21
- package/dist/resource/instance.js +94 -62
- package/dist/resource/schema.d.ts +155 -10
- package/dist/resource/schema.js +108 -5
- package/dist/resource/tags.d.ts +14 -0
- package/dist/types.d.ts +9 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @veloxts/router
|
|
2
2
|
|
|
3
|
+
## 0.6.99
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updates to documentation and mark as beta 0.6.x
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @veloxts/core@0.6.99
|
|
10
|
+
- @veloxts/validation@0.6.99
|
|
11
|
+
|
|
12
|
+
## 0.6.98
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- feat(router): nested resource schema relations with .hasOne() / .hasMany()
|
|
17
|
+
- Updated dependencies
|
|
18
|
+
- @veloxts/core@0.6.98
|
|
19
|
+
- @veloxts/validation@0.6.98
|
|
20
|
+
|
|
3
21
|
## 0.6.97
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @veloxts/router
|
|
2
2
|
|
|
3
|
-
> **
|
|
3
|
+
> **Beta (v0.6.x)**
|
|
4
4
|
|
|
5
5
|
Procedure-based routing for VeloxTS Framework - provides hybrid tRPC and REST adapters with naming convention-based HTTP method inference. Learn more at [@veloxts/velox](https://www.npmjs.com/package/@veloxts/velox).
|
|
6
6
|
|
package/dist/index.d.ts
CHANGED
|
@@ -83,7 +83,7 @@ export { serve } from './expose.js';
|
|
|
83
83
|
*/
|
|
84
84
|
export type { BuildParametersOptions, BuildParametersResult, GuardMappingOptions, JSONSchema, OpenAPIComponents, OpenAPIContact, OpenAPIEncoding, OpenAPIExample, OpenAPIExternalDocs, OpenAPIGeneratorOptions, OpenAPIHeader, OpenAPIHttpMethod, OpenAPIInfo, OpenAPILicense, OpenAPILink, OpenAPIMediaType, OpenAPIOAuthFlow, OpenAPIOAuthFlows, OpenAPIOperation, OpenAPIParameter, OpenAPIPathItem, OpenAPIRequestBody, OpenAPIResponse, OpenAPISecurityRequirement, OpenAPISecurityScheme, OpenAPIServer, OpenAPISpec, OpenAPITag, ParameterIn, QueryParamExtractionOptions, RouteInfo, SchemaConversionOptions, SecuritySchemeType, SwaggerUIConfig, SwaggerUIHtmlOptions, SwaggerUIPluginOptions, } from './openapi/index.js';
|
|
85
85
|
export { buildParameters, convertFromOpenAPIPath, convertToOpenAPIPath, createSecurityRequirement, createStringSchema, createSwaggerUI, DEFAULT_GUARD_MAPPINGS, DEFAULT_SECURITY_SCHEMES, DEFAULT_UI_CONFIG, escapeHtml, extractGuardScopes, extractPathParamNames, extractQueryParameters, extractResourceFromPath, extractSchemaProperties, extractUsedSecuritySchemes, filterUsedSecuritySchemes, generateOpenApiSpec, generateSwaggerUIHtml, getOpenApiRouteSummary, getOpenApiSpec, guardsRequireAuth, guardsToSecurity, hasPathParameters, joinPaths, mapGuardToSecurity, mergeSchemas, mergeSecuritySchemes, normalizePath, parsePathParameters, registerDocs, removeSchemaProperties, SWAGGER_UI_CDN, schemaHasProperties, swaggerUIPlugin, validateOpenApiSpec, zodSchemaToJsonSchema, } from './openapi/index.js';
|
|
86
|
-
export type { AccessLevel, ADMIN, AdminOutput, AdminTaggedContext, ANONYMOUS, AnonymousOutput, AnonymousTaggedContext, AnyResourceOutput, AUTHENTICATED, AuthenticatedOutput, AuthenticatedTaggedContext, ContextTag, ExtractTag, HasTag, IfAdmin, IfAuthenticated, InferResourceData, InferResourceOutput, IsVisibleToTag, OutputForTag, ResourceField, ResourceSchema, RuntimeField, TaggedContext, VisibilityLevel, WithTag, } from './resource/index.js';
|
|
86
|
+
export type { AccessLevel, ADMIN, AdminOutput, AdminTaggedContext, ANONYMOUS, AnonymousOutput, AnonymousTaggedContext, AnyResourceOutput, AUTHENTICATED, AuthenticatedOutput, AuthenticatedTaggedContext, BuilderField, ContextTag, ExtractTag, HasTag, IfAdmin, IfAuthenticated, InferResourceData, InferResourceOutput, IsVisibleToTag, OutputForTag, RelationField, ResourceField, ResourceSchema, RuntimeField, TaggedContext, VisibilityLevel, WithTag, } from './resource/index.js';
|
|
87
87
|
/**
|
|
88
88
|
* Resource API for context-dependent output types using phantom types.
|
|
89
89
|
*
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { ConfigurationError, logWarning } from '@veloxts/core';
|
|
11
11
|
import { GuardError } from '../errors.js';
|
|
12
12
|
import { createMiddlewareExecutor, executeMiddlewareChain } from '../middleware/chain.js';
|
|
13
|
-
import { Resource, } from '../resource/index.js';
|
|
13
|
+
import { isTaggedResourceSchema, Resource, } from '../resource/index.js';
|
|
14
14
|
import { deriveParentParamName } from '../utils/pluralization.js';
|
|
15
15
|
import { analyzeNamingConvention, isDevelopment, normalizeWarningOption, } from '../warnings.js';
|
|
16
16
|
// ============================================================================
|
|
@@ -202,15 +202,35 @@ function createBuilder(state) {
|
|
|
202
202
|
/**
|
|
203
203
|
* Sets the output type based on a resource schema
|
|
204
204
|
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
205
|
+
* Accepts either a plain `ResourceSchema` or a tagged schema
|
|
206
|
+
* (e.g., `UserSchema.authenticated`) for declarative auto-projection.
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```typescript
|
|
210
|
+
* // Tagged schema — auto-projects to authenticated fields
|
|
211
|
+
* procedure()
|
|
212
|
+
* .guard(authenticated)
|
|
213
|
+
* .resource(UserSchema.authenticated)
|
|
214
|
+
* .query(handler);
|
|
215
|
+
*
|
|
216
|
+
* // Plain schema — defaults to public, or derives from guardNarrow
|
|
217
|
+
* procedure()
|
|
218
|
+
* .resource(UserSchema)
|
|
219
|
+
* .query(handler);
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
/**
|
|
223
|
+
* Sets the output type based on a resource schema
|
|
224
|
+
*
|
|
225
|
+
* Accepts either a plain `ResourceSchema` or a tagged schema
|
|
226
|
+
* (e.g., `UserSchema.authenticated`) for declarative auto-projection.
|
|
207
227
|
*/
|
|
208
228
|
resource(schema) {
|
|
209
|
-
|
|
210
|
-
// The actual output type is computed at the type level
|
|
229
|
+
const level = isTaggedResourceSchema(schema) ? schema._level : undefined;
|
|
211
230
|
return createBuilder({
|
|
212
231
|
...state,
|
|
213
232
|
resourceSchema: schema,
|
|
233
|
+
resourceLevel: level,
|
|
214
234
|
});
|
|
215
235
|
},
|
|
216
236
|
};
|
|
@@ -248,6 +268,8 @@ function compileProcedure(type, handler, state) {
|
|
|
248
268
|
_precompiledExecutor: precompiledExecutor,
|
|
249
269
|
// Store resource schema for auto-projection
|
|
250
270
|
_resourceSchema: state.resourceSchema,
|
|
271
|
+
// Store explicit resource level from tagged schema (e.g., UserSchema.authenticated)
|
|
272
|
+
_resourceLevel: state.resourceLevel,
|
|
251
273
|
};
|
|
252
274
|
}
|
|
253
275
|
/**
|
|
@@ -460,17 +482,24 @@ export async function executeProcedure(procedure, rawInput, ctx) {
|
|
|
460
482
|
// Step 4: Auto-project if resource schema is set
|
|
461
483
|
if (procedure._resourceSchema) {
|
|
462
484
|
const schema = procedure._resourceSchema;
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
485
|
+
// Prefer explicit level from tagged schema over guard-derived level
|
|
486
|
+
const finalLevel = procedure._resourceLevel ?? accessLevel;
|
|
487
|
+
const projectOne = (item) => {
|
|
488
|
+
const r = new Resource(item, schema);
|
|
489
|
+
switch (finalLevel) {
|
|
490
|
+
case 'admin':
|
|
491
|
+
return r.forAdmin();
|
|
492
|
+
case 'authenticated':
|
|
493
|
+
return r.forAuthenticated();
|
|
494
|
+
default:
|
|
495
|
+
return r.forAnonymous();
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
if (Array.isArray(result)) {
|
|
499
|
+
result = result.map((item) => projectOne(item));
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
result = projectOne(result);
|
|
474
503
|
}
|
|
475
504
|
}
|
|
476
505
|
// Step 5: Validate output if schema provided
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { BaseContext } from '@veloxts/core';
|
|
13
13
|
import type { ZodType, ZodTypeDef } from 'zod';
|
|
14
|
-
import type { OutputForTag, ResourceSchema } from '../resource/index.js';
|
|
15
|
-
import type { ContextTag, ExtractTag, TaggedContext } from '../resource/tags.js';
|
|
14
|
+
import type { OutputForTag, ResourceSchema, TaggedResourceSchema } from '../resource/index.js';
|
|
15
|
+
import type { ContextTag, ExtractTag, LevelToTag, TaggedContext } from '../resource/tags.js';
|
|
16
16
|
import type { CompiledProcedure, GuardLike, MiddlewareFunction, ParentResourceConfig, ProcedureHandler, RestRouteOverride } from '../types.js';
|
|
17
17
|
/**
|
|
18
18
|
* Internal state type that accumulates type information through the builder chain
|
|
@@ -372,34 +372,31 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
372
372
|
*/
|
|
373
373
|
mutation(handler: ProcedureHandler<TInput, TOutput, TContext>): CompiledProcedure<TInput, TOutput, TContext, 'mutation'>;
|
|
374
374
|
/**
|
|
375
|
-
* Sets the output type based on a resource schema
|
|
375
|
+
* Sets the output type based on a resource schema
|
|
376
376
|
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
* access level (anonymous, authenticated, or admin).
|
|
377
|
+
* Accepts either a tagged schema (e.g., `UserSchema.authenticated`) for
|
|
378
|
+
* explicit auto-projection, or a plain schema for backward compatibility.
|
|
380
379
|
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
* @template TSchema - The resource schema type
|
|
386
|
-
* @param schema - The resource schema defining field visibility
|
|
387
|
-
* @returns New builder with output type set based on context's tag
|
|
380
|
+
* When a tagged schema is used, the output type is computed from the
|
|
381
|
+
* tag's access level. When a plain schema is used, the output type is
|
|
382
|
+
* derived from the context's phantom tag (set by `guardNarrow`).
|
|
388
383
|
*
|
|
389
384
|
* @example
|
|
390
385
|
* ```typescript
|
|
391
|
-
* //
|
|
392
|
-
*
|
|
393
|
-
* .
|
|
394
|
-
* .
|
|
395
|
-
* .
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
386
|
+
* // Tagged schema — explicit projection level (recommended)
|
|
387
|
+
* procedure()
|
|
388
|
+
* .guard(authenticated)
|
|
389
|
+
* .resource(UserSchema.authenticated)
|
|
390
|
+
* .query(async ({ ctx }) => ctx.db.user.findUnique(...));
|
|
391
|
+
*
|
|
392
|
+
* // Plain schema — derives level from guardNarrow or defaults to public
|
|
393
|
+
* procedure()
|
|
394
|
+
* .guardNarrow(authenticatedNarrow)
|
|
395
|
+
* .resource(UserSchema)
|
|
396
|
+
* .query(async ({ ctx }) => ctx.db.user.findUnique(...));
|
|
400
397
|
* ```
|
|
401
398
|
*/
|
|
402
|
-
resource<TSchema extends ResourceSchema>(schema: TSchema): ProcedureBuilder<TInput, TContext extends TaggedContext<infer TTag> ? TTag extends ContextTag ? OutputForTag<TSchema, TTag> : OutputForTag<TSchema, ExtractTag<TContext>> : OutputForTag<TSchema, ExtractTag<TContext>>, TContext>;
|
|
399
|
+
resource<TSchema extends ResourceSchema>(schema: TSchema): ProcedureBuilder<TInput, TSchema extends TaggedResourceSchema<infer TFields, infer TLevel> ? OutputForTag<ResourceSchema<TFields>, LevelToTag<TLevel>> : TContext extends TaggedContext<infer TTag> ? TTag extends ContextTag ? OutputForTag<TSchema, TTag> : OutputForTag<TSchema, ExtractTag<TContext>> : OutputForTag<TSchema, ExtractTag<TContext>>, TContext>;
|
|
403
400
|
}
|
|
404
401
|
/**
|
|
405
402
|
* Internal runtime state for the procedure builder
|
|
@@ -414,6 +411,8 @@ export interface BuilderRuntimeState {
|
|
|
414
411
|
outputSchema?: ValidSchema;
|
|
415
412
|
/** Resource schema for context-dependent output */
|
|
416
413
|
resourceSchema?: ResourceSchema;
|
|
414
|
+
/** Explicit resource level from tagged schema (e.g., UserSchema.authenticated) */
|
|
415
|
+
resourceLevel?: 'public' | 'authenticated' | 'admin';
|
|
417
416
|
/** Middleware chain */
|
|
418
417
|
middlewares: MiddlewareFunction<unknown, BaseContext, BaseContext, unknown>[];
|
|
419
418
|
/** Guards to execute before handler */
|
package/dist/resource/index.d.ts
CHANGED
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
* @module resource
|
|
61
61
|
*/
|
|
62
62
|
export { Resource, ResourceCollection, resource, resourceCollection } from './instance.js';
|
|
63
|
-
export type { AdminOutput, AnonymousOutput, AuthenticatedOutput, OutputForTag, ResourceField, ResourceSchema, RuntimeField, } from './schema.js';
|
|
64
|
-
export { isResourceSchema, ResourceSchemaBuilder, resourceSchema } from './schema.js';
|
|
65
|
-
export type { AccessLevel, ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag, ExtractTag, HasTag, TaggedContext, WithTag, } from './tags.js';
|
|
63
|
+
export type { AdminOutput, AnonymousOutput, AuthenticatedOutput, BuilderField, OutputForLevel, OutputForTag, RelationField, ResourceField, ResourceSchema, ResourceSchemaWithViews, RuntimeField, TaggedResourceSchema, } from './schema.js';
|
|
64
|
+
export { isResourceSchema, isTaggedResourceSchema, ResourceSchemaBuilder, resourceSchema, } from './schema.js';
|
|
65
|
+
export type { AccessLevel, ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag, ExtractTag, HasTag, LevelToTag, TaggedContext, WithTag, } from './tags.js';
|
|
66
66
|
export type { AdminTaggedContext, AnonymousTaggedContext, AnyResourceOutput, AuthenticatedTaggedContext, IfAdmin, IfAuthenticated, InferResourceData, InferResourceOutput, } from './types.js';
|
|
67
67
|
export type { IsVisibleToTag, VisibilityLevel } from './visibility.js';
|
|
68
68
|
export { getAccessibleLevels, getVisibilityForTag, isVisibleAtLevel } from './visibility.js';
|
package/dist/resource/index.js
CHANGED
|
@@ -65,6 +65,6 @@
|
|
|
65
65
|
// Resource instances
|
|
66
66
|
export { Resource, ResourceCollection, resource, resourceCollection } from './instance.js';
|
|
67
67
|
// Schema builder
|
|
68
|
-
export { isResourceSchema, ResourceSchemaBuilder, resourceSchema } from './schema.js';
|
|
68
|
+
export { isResourceSchema, isTaggedResourceSchema, ResourceSchemaBuilder, resourceSchema, } from './schema.js';
|
|
69
69
|
// Visibility
|
|
70
70
|
export { getAccessibleLevels, getVisibilityForTag, isVisibleAtLevel } from './visibility.js';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module resource/instance
|
|
8
8
|
*/
|
|
9
|
-
import type { OutputForTag, ResourceSchema } from './schema.js';
|
|
9
|
+
import type { OutputForLevel, OutputForTag, ResourceSchema, TaggedResourceSchema } from './schema.js';
|
|
10
10
|
import type { ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag, ExtractTag, TaggedContext } from './tags.js';
|
|
11
11
|
/**
|
|
12
12
|
* A resource instance that can project data based on access level
|
|
@@ -73,12 +73,11 @@ export declare class Resource<TSchema extends ResourceSchema> {
|
|
|
73
73
|
/**
|
|
74
74
|
* Projects data for an explicit visibility level
|
|
75
75
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* prototype pollution attacks.
|
|
76
|
+
* Delegates to the standalone `projectData()` function which supports
|
|
77
|
+
* recursive projection of nested relations.
|
|
79
78
|
*
|
|
80
79
|
* @param level - The visibility level to project for
|
|
81
|
-
* @returns Object with only the visible fields
|
|
80
|
+
* @returns Object with only the visible fields (null prototype)
|
|
82
81
|
*/
|
|
83
82
|
private _project;
|
|
84
83
|
/**
|
|
@@ -141,44 +140,53 @@ export declare class ResourceCollection<TSchema extends ResourceSchema> {
|
|
|
141
140
|
isEmpty(): boolean;
|
|
142
141
|
}
|
|
143
142
|
/**
|
|
144
|
-
* Creates a Resource instance for a single data object
|
|
143
|
+
* Creates a projected view or Resource instance for a single data object
|
|
144
|
+
*
|
|
145
|
+
* When called with a tagged schema (e.g., `UserSchema.authenticated`),
|
|
146
|
+
* returns the projected data directly. When called with an untagged schema,
|
|
147
|
+
* returns a Resource instance with `.forAnonymous()`, `.forAuthenticated()`,
|
|
148
|
+
* `.forAdmin()` methods.
|
|
145
149
|
*
|
|
146
150
|
* @param data - The raw data object
|
|
147
|
-
* @param schema -
|
|
148
|
-
* @returns
|
|
151
|
+
* @param schema - Resource schema (tagged for direct projection, untagged for Resource instance)
|
|
152
|
+
* @returns Projected data or Resource instance
|
|
149
153
|
*
|
|
150
154
|
* @example
|
|
151
155
|
* ```typescript
|
|
152
156
|
* const user = await db.user.findUnique({ where: { id } });
|
|
153
|
-
* if (!user) throw new NotFoundError();
|
|
154
157
|
*
|
|
155
|
-
* //
|
|
156
|
-
* return resource(user, UserSchema
|
|
158
|
+
* // Tagged schema → returns projected data directly
|
|
159
|
+
* return resource(user, UserSchema.authenticated);
|
|
160
|
+
* // → { id, name, email }
|
|
157
161
|
*
|
|
158
|
-
* //
|
|
162
|
+
* // Untagged schema → returns Resource with projection methods
|
|
159
163
|
* return resource(user, UserSchema).forAuthenticated();
|
|
160
|
-
*
|
|
161
|
-
* // In an admin endpoint
|
|
162
|
-
* return resource(user, UserSchema).forAdmin();
|
|
164
|
+
* // → { id, name, email }
|
|
163
165
|
* ```
|
|
164
166
|
*/
|
|
167
|
+
export declare function resource<TSchema extends TaggedResourceSchema>(data: Record<string, unknown>, schema: TSchema): OutputForLevel<TSchema>;
|
|
165
168
|
export declare function resource<TSchema extends ResourceSchema>(data: Record<string, unknown>, schema: TSchema): Resource<TSchema>;
|
|
166
169
|
/**
|
|
167
|
-
* Creates a ResourceCollection for an array of data objects
|
|
170
|
+
* Creates a projected array or ResourceCollection for an array of data objects
|
|
171
|
+
*
|
|
172
|
+
* When called with a tagged schema (e.g., `UserSchema.authenticated`),
|
|
173
|
+
* returns the projected array directly. When called with an untagged schema,
|
|
174
|
+
* returns a ResourceCollection with projection methods.
|
|
168
175
|
*
|
|
169
176
|
* @param items - Array of raw data objects
|
|
170
|
-
* @param schema -
|
|
171
|
-
* @returns
|
|
177
|
+
* @param schema - Resource schema (tagged for direct projection, untagged for collection)
|
|
178
|
+
* @returns Projected array or ResourceCollection instance
|
|
172
179
|
*
|
|
173
180
|
* @example
|
|
174
181
|
* ```typescript
|
|
175
182
|
* const users = await db.user.findMany({ take: 10 });
|
|
176
183
|
*
|
|
177
|
-
* //
|
|
178
|
-
* return resourceCollection(users, UserSchema
|
|
184
|
+
* // Tagged schema → returns projected array directly
|
|
185
|
+
* return resourceCollection(users, UserSchema.authenticated);
|
|
179
186
|
*
|
|
180
|
-
* //
|
|
187
|
+
* // Untagged schema → returns ResourceCollection with methods
|
|
181
188
|
* return resourceCollection(users, UserSchema).forAuthenticated();
|
|
182
189
|
* ```
|
|
183
190
|
*/
|
|
191
|
+
export declare function resourceCollection<TSchema extends TaggedResourceSchema>(items: Array<Record<string, unknown>>, schema: TSchema): Array<OutputForLevel<TSchema>>;
|
|
184
192
|
export declare function resourceCollection<TSchema extends ResourceSchema>(items: Array<Record<string, unknown>>, schema: TSchema): ResourceCollection<TSchema>;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module resource/instance
|
|
8
8
|
*/
|
|
9
|
+
import { isTaggedResourceSchema } from './schema.js';
|
|
9
10
|
import { isVisibleAtLevel } from './visibility.js';
|
|
10
11
|
// ============================================================================
|
|
11
12
|
// Security Constants
|
|
@@ -24,6 +25,71 @@ const DANGEROUS_PROPERTIES = new Set([
|
|
|
24
25
|
'__lookupSetter__',
|
|
25
26
|
]);
|
|
26
27
|
// ============================================================================
|
|
28
|
+
// Recursive Projection
|
|
29
|
+
// ============================================================================
|
|
30
|
+
/** Maximum allowed nesting depth for recursive relation projection */
|
|
31
|
+
const MAX_PROJECTION_DEPTH = 10;
|
|
32
|
+
/**
|
|
33
|
+
* Recursively projects data through a resource schema, including nested relations
|
|
34
|
+
*
|
|
35
|
+
* Tracks recursion depth and visited objects to guard against circular
|
|
36
|
+
* references and excessively deep nesting.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
* @param data - The raw data object
|
|
40
|
+
* @param schema - The resource schema to project against
|
|
41
|
+
* @param level - The visibility level to project for
|
|
42
|
+
* @param depth - Current recursion depth (defaults to 0)
|
|
43
|
+
* @param visited - Set of already-visited data objects for cycle detection
|
|
44
|
+
* @returns Projected object with null prototype
|
|
45
|
+
*/
|
|
46
|
+
function projectData(data, schema, level, depth = 0, visited) {
|
|
47
|
+
if (depth >= MAX_PROJECTION_DEPTH) {
|
|
48
|
+
return Object.create(null);
|
|
49
|
+
}
|
|
50
|
+
const seen = visited ?? new WeakSet();
|
|
51
|
+
if (seen.has(data)) {
|
|
52
|
+
return Object.create(null);
|
|
53
|
+
}
|
|
54
|
+
seen.add(data);
|
|
55
|
+
const result = Object.create(null);
|
|
56
|
+
for (const field of schema.fields) {
|
|
57
|
+
if (!isVisibleAtLevel(field.visibility, level))
|
|
58
|
+
continue;
|
|
59
|
+
if (DANGEROUS_PROPERTIES.has(field.name))
|
|
60
|
+
continue;
|
|
61
|
+
const value = data[field.name];
|
|
62
|
+
if (field.nestedSchema && field.cardinality) {
|
|
63
|
+
// Relation field — recursive projection
|
|
64
|
+
if (field.cardinality === 'one') {
|
|
65
|
+
if (value != null && typeof value === 'object' && !Array.isArray(value)) {
|
|
66
|
+
result[field.name] = projectData(value, field.nestedSchema, level, depth + 1, seen);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
result[field.name] = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// 'many' — only project valid object items; skip primitives / nulls
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
const nested = field.nestedSchema;
|
|
76
|
+
result[field.name] = value
|
|
77
|
+
.filter((item) => item != null && typeof item === 'object' && !Array.isArray(item))
|
|
78
|
+
.map((item) => projectData(item, nested, level, depth + 1, seen));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
result[field.name] = [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (value !== undefined) {
|
|
86
|
+
// Scalar field — existing behavior
|
|
87
|
+
result[field.name] = value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
// ============================================================================
|
|
27
93
|
// Resource Instance
|
|
28
94
|
// ============================================================================
|
|
29
95
|
/**
|
|
@@ -105,29 +171,14 @@ export class Resource {
|
|
|
105
171
|
/**
|
|
106
172
|
* Projects data for an explicit visibility level
|
|
107
173
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* prototype pollution attacks.
|
|
174
|
+
* Delegates to the standalone `projectData()` function which supports
|
|
175
|
+
* recursive projection of nested relations.
|
|
111
176
|
*
|
|
112
177
|
* @param level - The visibility level to project for
|
|
113
|
-
* @returns Object with only the visible fields
|
|
178
|
+
* @returns Object with only the visible fields (null prototype)
|
|
114
179
|
*/
|
|
115
180
|
_project(level) {
|
|
116
|
-
|
|
117
|
-
const result = Object.create(null);
|
|
118
|
-
for (const field of this._schema.fields) {
|
|
119
|
-
if (isVisibleAtLevel(field.visibility, level)) {
|
|
120
|
-
// Skip dangerous prototype properties to prevent pollution attacks
|
|
121
|
-
if (DANGEROUS_PROPERTIES.has(field.name)) {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
const value = this._data[field.name];
|
|
125
|
-
if (value !== undefined) {
|
|
126
|
-
result[field.name] = value;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return result;
|
|
181
|
+
return projectData(this._data, this._schema, level);
|
|
131
182
|
}
|
|
132
183
|
/**
|
|
133
184
|
* Infers the visibility level from a context object
|
|
@@ -231,52 +282,33 @@ export class ResourceCollection {
|
|
|
231
282
|
return this._items.length === 0;
|
|
232
283
|
}
|
|
233
284
|
}
|
|
234
|
-
// ============================================================================
|
|
235
|
-
// Factory Functions
|
|
236
|
-
// ============================================================================
|
|
237
|
-
/**
|
|
238
|
-
* Creates a Resource instance for a single data object
|
|
239
|
-
*
|
|
240
|
-
* @param data - The raw data object
|
|
241
|
-
* @param schema - The resource schema defining field visibility
|
|
242
|
-
* @returns Resource instance with projection methods
|
|
243
|
-
*
|
|
244
|
-
* @example
|
|
245
|
-
* ```typescript
|
|
246
|
-
* const user = await db.user.findUnique({ where: { id } });
|
|
247
|
-
* if (!user) throw new NotFoundError();
|
|
248
|
-
*
|
|
249
|
-
* // In a public endpoint
|
|
250
|
-
* return resource(user, UserSchema).forAnonymous();
|
|
251
|
-
*
|
|
252
|
-
* // In an authenticated endpoint
|
|
253
|
-
* return resource(user, UserSchema).forAuthenticated();
|
|
254
|
-
*
|
|
255
|
-
* // In an admin endpoint
|
|
256
|
-
* return resource(user, UserSchema).forAdmin();
|
|
257
|
-
* ```
|
|
258
|
-
*/
|
|
259
285
|
export function resource(data, schema) {
|
|
286
|
+
if (isTaggedResourceSchema(schema)) {
|
|
287
|
+
const r = new Resource(data, schema);
|
|
288
|
+
switch (schema._level) {
|
|
289
|
+
case 'admin':
|
|
290
|
+
return r.forAdmin();
|
|
291
|
+
case 'authenticated':
|
|
292
|
+
return r.forAuthenticated();
|
|
293
|
+
default:
|
|
294
|
+
return r.forAnonymous();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
260
297
|
return new Resource(data, schema);
|
|
261
298
|
}
|
|
262
|
-
/**
|
|
263
|
-
* Creates a ResourceCollection for an array of data objects
|
|
264
|
-
*
|
|
265
|
-
* @param items - Array of raw data objects
|
|
266
|
-
* @param schema - The resource schema defining field visibility
|
|
267
|
-
* @returns ResourceCollection instance with batch projection methods
|
|
268
|
-
*
|
|
269
|
-
* @example
|
|
270
|
-
* ```typescript
|
|
271
|
-
* const users = await db.user.findMany({ take: 10 });
|
|
272
|
-
*
|
|
273
|
-
* // In a public endpoint
|
|
274
|
-
* return resourceCollection(users, UserSchema).forAnonymous();
|
|
275
|
-
*
|
|
276
|
-
* // In an authenticated endpoint
|
|
277
|
-
* return resourceCollection(users, UserSchema).forAuthenticated();
|
|
278
|
-
* ```
|
|
279
|
-
*/
|
|
280
299
|
export function resourceCollection(items, schema) {
|
|
300
|
+
if (isTaggedResourceSchema(schema)) {
|
|
301
|
+
return items.map((item) => {
|
|
302
|
+
const r = new Resource(item, schema);
|
|
303
|
+
switch (schema._level) {
|
|
304
|
+
case 'admin':
|
|
305
|
+
return r.forAdmin();
|
|
306
|
+
case 'authenticated':
|
|
307
|
+
return r.forAuthenticated();
|
|
308
|
+
default:
|
|
309
|
+
return r.forAnonymous();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
281
313
|
return new ResourceCollection(items, schema);
|
|
282
314
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @module resource/schema
|
|
9
9
|
*/
|
|
10
10
|
import type { ZodType, ZodTypeDef } from 'zod';
|
|
11
|
-
import type { ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag } from './tags.js';
|
|
11
|
+
import type { AccessLevel, ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag, LevelToTag } from './tags.js';
|
|
12
12
|
import type { IsVisibleToTag, VisibilityLevel } from './visibility.js';
|
|
13
13
|
/**
|
|
14
14
|
* A single field definition in a resource schema
|
|
@@ -25,13 +25,35 @@ export interface ResourceField<TName extends string = string, TSchema extends Zo
|
|
|
25
25
|
/** Visibility level */
|
|
26
26
|
readonly visibility: TLevel;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* A relation field that references a nested resource schema
|
|
30
|
+
*
|
|
31
|
+
* @template TName - The field name as a literal type
|
|
32
|
+
* @template TNestedFields - The nested schema's field definitions
|
|
33
|
+
* @template TLevel - The visibility level controlling WHETHER the relation is included
|
|
34
|
+
* @template TCardinality - 'one' for nullable object, 'many' for array
|
|
35
|
+
*/
|
|
36
|
+
export interface RelationField<TName extends string = string, TNestedFields extends readonly BuilderField[] = readonly BuilderField[], TLevel extends VisibilityLevel = VisibilityLevel, TCardinality extends 'one' | 'many' = 'one' | 'many'> {
|
|
37
|
+
readonly name: TName;
|
|
38
|
+
readonly nestedSchema: ResourceSchema<TNestedFields>;
|
|
39
|
+
readonly visibility: TLevel;
|
|
40
|
+
readonly cardinality: TCardinality;
|
|
41
|
+
/** Brand discriminator — distinguishes from ResourceField */
|
|
42
|
+
readonly _relation: true;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Union of scalar and relation fields in a resource schema
|
|
46
|
+
*/
|
|
47
|
+
export type BuilderField = ResourceField | RelationField;
|
|
28
48
|
/**
|
|
29
49
|
* Runtime representation of a field (without generics for storage)
|
|
30
50
|
*/
|
|
31
51
|
export interface RuntimeField {
|
|
32
52
|
readonly name: string;
|
|
33
|
-
readonly schema
|
|
53
|
+
readonly schema?: ZodType;
|
|
34
54
|
readonly visibility: VisibilityLevel;
|
|
55
|
+
readonly nestedSchema?: ResourceSchema;
|
|
56
|
+
readonly cardinality?: 'one' | 'many';
|
|
35
57
|
}
|
|
36
58
|
/**
|
|
37
59
|
* A completed resource schema with all field definitions
|
|
@@ -41,12 +63,65 @@ export interface RuntimeField {
|
|
|
41
63
|
*
|
|
42
64
|
* @template TFields - Tuple of ResourceField types
|
|
43
65
|
*/
|
|
44
|
-
export interface ResourceSchema<TFields extends readonly
|
|
66
|
+
export interface ResourceSchema<TFields extends readonly BuilderField[] = readonly BuilderField[]> {
|
|
45
67
|
/** Runtime field definitions */
|
|
46
68
|
readonly fields: readonly RuntimeField[];
|
|
47
69
|
/** Phantom type for field type information */
|
|
48
70
|
readonly __fields?: TFields;
|
|
49
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* A resource schema tagged with an explicit access level
|
|
74
|
+
*
|
|
75
|
+
* Created by accessing `.public`, `.authenticated`, or `.admin` on a built schema.
|
|
76
|
+
* Used in both procedure definitions (auto-projection) and handler-level
|
|
77
|
+
* projection via `resource(data, Schema.authenticated)`.
|
|
78
|
+
*
|
|
79
|
+
* @template TFields - The field definitions from the base schema
|
|
80
|
+
* @template TLevel - The access level for projection
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const UserSchema = resourceSchema()
|
|
85
|
+
* .public('id', z.string())
|
|
86
|
+
* .authenticated('email', z.string())
|
|
87
|
+
* .build();
|
|
88
|
+
*
|
|
89
|
+
* // Tagged views
|
|
90
|
+
* UserSchema.public // TaggedResourceSchema<..., 'public'>
|
|
91
|
+
* UserSchema.authenticated // TaggedResourceSchema<..., 'authenticated'>
|
|
92
|
+
* UserSchema.admin // TaggedResourceSchema<..., 'admin'>
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export interface TaggedResourceSchema<TFields extends readonly BuilderField[] = readonly BuilderField[], TLevel extends AccessLevel = AccessLevel> extends ResourceSchema<TFields> {
|
|
96
|
+
readonly _level: TLevel;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A completed resource schema with pre-built tagged views
|
|
100
|
+
*
|
|
101
|
+
* Returned by `resourceSchema().build()`. Extends `ResourceSchema` (backward
|
|
102
|
+
* compatible) and adds `.public`, `.authenticated`, `.admin` properties
|
|
103
|
+
* for declarative projection.
|
|
104
|
+
*
|
|
105
|
+
* @template TFields - The field definitions
|
|
106
|
+
*/
|
|
107
|
+
export interface ResourceSchemaWithViews<TFields extends readonly BuilderField[] = readonly BuilderField[]> extends ResourceSchema<TFields> {
|
|
108
|
+
readonly public: TaggedResourceSchema<TFields, 'public'>;
|
|
109
|
+
readonly authenticated: TaggedResourceSchema<TFields, 'authenticated'>;
|
|
110
|
+
readonly admin: TaggedResourceSchema<TFields, 'admin'>;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Computes the output type for a tagged resource schema
|
|
114
|
+
*
|
|
115
|
+
* Maps the access level to the corresponding phantom tag and
|
|
116
|
+
* computes the projected output type.
|
|
117
|
+
*
|
|
118
|
+
* @template TSchema - A tagged resource schema
|
|
119
|
+
*/
|
|
120
|
+
export type OutputForLevel<TSchema extends TaggedResourceSchema> = TSchema extends TaggedResourceSchema<infer TFields, infer TLevel> ? OutputForTag<ResourceSchema<TFields>, LevelToTag<TLevel>> : never;
|
|
121
|
+
/**
|
|
122
|
+
* Type guard to check if a schema is a TaggedResourceSchema (has _level)
|
|
123
|
+
*/
|
|
124
|
+
export declare function isTaggedResourceSchema(value: unknown): value is TaggedResourceSchema;
|
|
50
125
|
/**
|
|
51
126
|
* Helper to infer the output type of a Zod schema
|
|
52
127
|
*/
|
|
@@ -55,11 +130,14 @@ type InferZodOutput<T> = T extends ZodType<infer O, ZodTypeDef, unknown> ? O : n
|
|
|
55
130
|
* Filters fields by visibility and extracts their types
|
|
56
131
|
*
|
|
57
132
|
* This type iterates over the fields tuple and includes only those
|
|
58
|
-
* that are visible to the given context tag.
|
|
133
|
+
* that are visible to the given context tag. RelationField entries
|
|
134
|
+
* are recursively projected using the same tag.
|
|
59
135
|
*/
|
|
60
|
-
type FilterFieldsByTag<TFields extends readonly
|
|
136
|
+
type FilterFieldsByTag<TFields extends readonly BuilderField[], TTag extends ContextTag> = TFields extends readonly [infer First, ...infer Rest] ? Rest extends readonly BuilderField[] ? First extends RelationField<infer Name, infer NestedFields, infer Level, infer Card> ? IsVisibleToTag<Level, TTag> extends true ? {
|
|
137
|
+
[K in Name]: Card extends 'one' ? Simplify<FilterFieldsByTag<NestedFields, TTag>> | null : Array<Simplify<FilterFieldsByTag<NestedFields, TTag>>>;
|
|
138
|
+
} & FilterFieldsByTag<Rest, TTag> : FilterFieldsByTag<Rest, TTag> : First extends ResourceField<infer Name, infer Schema, infer Level> ? IsVisibleToTag<Level, TTag> extends true ? {
|
|
61
139
|
[K in Name]: InferZodOutput<Schema>;
|
|
62
|
-
} & FilterFieldsByTag<Rest, TTag> : FilterFieldsByTag<Rest, TTag> :
|
|
140
|
+
} & FilterFieldsByTag<Rest, TTag> : FilterFieldsByTag<Rest, TTag> : FilterFieldsByTag<Rest, TTag> : unknown : unknown;
|
|
63
141
|
/**
|
|
64
142
|
* Simplifies an intersection type to a cleaner object type
|
|
65
143
|
*
|
|
@@ -108,7 +186,7 @@ export type AdminOutput<TSchema extends ResourceSchema> = OutputForTag<TSchema,
|
|
|
108
186
|
* .build();
|
|
109
187
|
* ```
|
|
110
188
|
*/
|
|
111
|
-
export declare class ResourceSchemaBuilder<TFields extends readonly
|
|
189
|
+
export declare class ResourceSchemaBuilder<TFields extends readonly BuilderField[] = readonly []> {
|
|
112
190
|
private readonly _fields;
|
|
113
191
|
private constructor();
|
|
114
192
|
/**
|
|
@@ -139,6 +217,56 @@ export declare class ResourceSchemaBuilder<TFields extends readonly ResourceFiel
|
|
|
139
217
|
* @returns New builder with the field added
|
|
140
218
|
*/
|
|
141
219
|
admin<TName extends string, TSchema extends ZodType>(name: TName, schema: TSchema): ResourceSchemaBuilder<readonly [...TFields, ResourceField<TName, TSchema, 'admin'>]>;
|
|
220
|
+
/**
|
|
221
|
+
* Adds a has-one relation (nullable nested object)
|
|
222
|
+
*
|
|
223
|
+
* The relation's visibility controls WHETHER it appears in the output.
|
|
224
|
+
* The parent's projection level controls WHAT fields of the nested schema are shown.
|
|
225
|
+
*
|
|
226
|
+
* **Note:** The nested schema's generic field types are tracked at compile time
|
|
227
|
+
* for output type computation, but the runtime field stores an untyped
|
|
228
|
+
* `ResourceSchema` reference. Always pass the direct result of `.build()`
|
|
229
|
+
* to ensure the compile-time and runtime schemas stay in sync.
|
|
230
|
+
*
|
|
231
|
+
* @param name - Relation field name
|
|
232
|
+
* @param nestedSchema - The nested resource schema (result of `.build()`)
|
|
233
|
+
* @param visibility - Visibility level for this relation
|
|
234
|
+
* @returns New builder with the relation added
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```typescript
|
|
238
|
+
* const UserSchema = resourceSchema()
|
|
239
|
+
* .public('id', z.string())
|
|
240
|
+
* .hasOne('organization', OrgSchema, 'public')
|
|
241
|
+
* .build();
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
hasOne<TName extends string, TNestedFields extends readonly BuilderField[], TLevel extends VisibilityLevel>(name: TName, nestedSchema: ResourceSchema<TNestedFields>, visibility: TLevel): ResourceSchemaBuilder<readonly [...TFields, RelationField<TName, TNestedFields, TLevel, 'one'>]>;
|
|
245
|
+
/**
|
|
246
|
+
* Adds a has-many relation (array of nested objects)
|
|
247
|
+
*
|
|
248
|
+
* The relation's visibility controls WHETHER it appears in the output.
|
|
249
|
+
* The parent's projection level controls WHAT fields of the nested schema are shown.
|
|
250
|
+
*
|
|
251
|
+
* **Note:** The nested schema's generic field types are tracked at compile time
|
|
252
|
+
* for output type computation, but the runtime field stores an untyped
|
|
253
|
+
* `ResourceSchema` reference. Always pass the direct result of `.build()`
|
|
254
|
+
* to ensure the compile-time and runtime schemas stay in sync.
|
|
255
|
+
*
|
|
256
|
+
* @param name - Relation field name
|
|
257
|
+
* @param nestedSchema - The nested resource schema (result of `.build()`)
|
|
258
|
+
* @param visibility - Visibility level for this relation
|
|
259
|
+
* @returns New builder with the relation added
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```typescript
|
|
263
|
+
* const UserSchema = resourceSchema()
|
|
264
|
+
* .public('id', z.string())
|
|
265
|
+
* .hasMany('posts', PostSchema, 'authenticated')
|
|
266
|
+
* .build();
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
hasMany<TName extends string, TNestedFields extends readonly BuilderField[], TLevel extends VisibilityLevel>(name: TName, nestedSchema: ResourceSchema<TNestedFields>, visibility: TLevel): ResourceSchemaBuilder<readonly [...TFields, RelationField<TName, TNestedFields, TLevel, 'many'>]>;
|
|
142
270
|
/**
|
|
143
271
|
* Adds a field with explicit visibility level
|
|
144
272
|
*
|
|
@@ -149,11 +277,28 @@ export declare class ResourceSchemaBuilder<TFields extends readonly ResourceFiel
|
|
|
149
277
|
*/
|
|
150
278
|
field<TName extends string, TSchema extends ZodType, TLevel extends VisibilityLevel>(name: TName, schema: TSchema, visibility: TLevel): ResourceSchemaBuilder<readonly [...TFields, ResourceField<TName, TSchema, TLevel>]>;
|
|
151
279
|
/**
|
|
152
|
-
* Builds the final resource schema
|
|
280
|
+
* Builds the final resource schema with tagged views
|
|
281
|
+
*
|
|
282
|
+
* Returns a schema with `.public`, `.authenticated`, and `.admin`
|
|
283
|
+
* properties for declarative projection in procedures.
|
|
284
|
+
*
|
|
285
|
+
* @returns Completed resource schema with tagged views
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```typescript
|
|
289
|
+
* const UserSchema = resourceSchema()
|
|
290
|
+
* .public('id', z.string())
|
|
291
|
+
* .authenticated('email', z.string())
|
|
292
|
+
* .build();
|
|
293
|
+
*
|
|
294
|
+
* // Use tagged views in procedures
|
|
295
|
+
* procedure().resource(UserSchema.authenticated).query(handler);
|
|
153
296
|
*
|
|
154
|
-
*
|
|
297
|
+
* // Or in handlers
|
|
298
|
+
* resource(data, UserSchema.authenticated);
|
|
299
|
+
* ```
|
|
155
300
|
*/
|
|
156
|
-
build():
|
|
301
|
+
build(): ResourceSchemaWithViews<TFields>;
|
|
157
302
|
}
|
|
158
303
|
/**
|
|
159
304
|
* Creates a new resource schema builder
|
package/dist/resource/schema.js
CHANGED
|
@@ -7,6 +7,18 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module resource/schema
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* Type guard to check if a schema is a TaggedResourceSchema (has _level)
|
|
12
|
+
*/
|
|
13
|
+
export function isTaggedResourceSchema(value) {
|
|
14
|
+
if (typeof value !== 'object' || value === null) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const obj = value;
|
|
18
|
+
return (Array.isArray(obj.fields) &&
|
|
19
|
+
typeof obj._level === 'string' &&
|
|
20
|
+
(obj._level === 'public' || obj._level === 'authenticated' || obj._level === 'admin'));
|
|
21
|
+
}
|
|
10
22
|
// ============================================================================
|
|
11
23
|
// Schema Builder
|
|
12
24
|
// ============================================================================
|
|
@@ -78,6 +90,76 @@ export class ResourceSchemaBuilder {
|
|
|
78
90
|
{ name, schema, visibility: 'admin' },
|
|
79
91
|
]);
|
|
80
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Adds a has-one relation (nullable nested object)
|
|
95
|
+
*
|
|
96
|
+
* The relation's visibility controls WHETHER it appears in the output.
|
|
97
|
+
* The parent's projection level controls WHAT fields of the nested schema are shown.
|
|
98
|
+
*
|
|
99
|
+
* **Note:** The nested schema's generic field types are tracked at compile time
|
|
100
|
+
* for output type computation, but the runtime field stores an untyped
|
|
101
|
+
* `ResourceSchema` reference. Always pass the direct result of `.build()`
|
|
102
|
+
* to ensure the compile-time and runtime schemas stay in sync.
|
|
103
|
+
*
|
|
104
|
+
* @param name - Relation field name
|
|
105
|
+
* @param nestedSchema - The nested resource schema (result of `.build()`)
|
|
106
|
+
* @param visibility - Visibility level for this relation
|
|
107
|
+
* @returns New builder with the relation added
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```typescript
|
|
111
|
+
* const UserSchema = resourceSchema()
|
|
112
|
+
* .public('id', z.string())
|
|
113
|
+
* .hasOne('organization', OrgSchema, 'public')
|
|
114
|
+
* .build();
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
hasOne(name, nestedSchema, visibility) {
|
|
118
|
+
return new ResourceSchemaBuilder([
|
|
119
|
+
...this._fields,
|
|
120
|
+
{
|
|
121
|
+
name,
|
|
122
|
+
visibility,
|
|
123
|
+
nestedSchema: nestedSchema,
|
|
124
|
+
cardinality: 'one',
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Adds a has-many relation (array of nested objects)
|
|
130
|
+
*
|
|
131
|
+
* The relation's visibility controls WHETHER it appears in the output.
|
|
132
|
+
* The parent's projection level controls WHAT fields of the nested schema are shown.
|
|
133
|
+
*
|
|
134
|
+
* **Note:** The nested schema's generic field types are tracked at compile time
|
|
135
|
+
* for output type computation, but the runtime field stores an untyped
|
|
136
|
+
* `ResourceSchema` reference. Always pass the direct result of `.build()`
|
|
137
|
+
* to ensure the compile-time and runtime schemas stay in sync.
|
|
138
|
+
*
|
|
139
|
+
* @param name - Relation field name
|
|
140
|
+
* @param nestedSchema - The nested resource schema (result of `.build()`)
|
|
141
|
+
* @param visibility - Visibility level for this relation
|
|
142
|
+
* @returns New builder with the relation added
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* const UserSchema = resourceSchema()
|
|
147
|
+
* .public('id', z.string())
|
|
148
|
+
* .hasMany('posts', PostSchema, 'authenticated')
|
|
149
|
+
* .build();
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
hasMany(name, nestedSchema, visibility) {
|
|
153
|
+
return new ResourceSchemaBuilder([
|
|
154
|
+
...this._fields,
|
|
155
|
+
{
|
|
156
|
+
name,
|
|
157
|
+
visibility,
|
|
158
|
+
nestedSchema: nestedSchema,
|
|
159
|
+
cardinality: 'many',
|
|
160
|
+
},
|
|
161
|
+
]);
|
|
162
|
+
}
|
|
81
163
|
/**
|
|
82
164
|
* Adds a field with explicit visibility level
|
|
83
165
|
*
|
|
@@ -93,14 +175,35 @@ export class ResourceSchemaBuilder {
|
|
|
93
175
|
]);
|
|
94
176
|
}
|
|
95
177
|
/**
|
|
96
|
-
* Builds the final resource schema
|
|
178
|
+
* Builds the final resource schema with tagged views
|
|
179
|
+
*
|
|
180
|
+
* Returns a schema with `.public`, `.authenticated`, and `.admin`
|
|
181
|
+
* properties for declarative projection in procedures.
|
|
182
|
+
*
|
|
183
|
+
* @returns Completed resource schema with tagged views
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* const UserSchema = resourceSchema()
|
|
188
|
+
* .public('id', z.string())
|
|
189
|
+
* .authenticated('email', z.string())
|
|
190
|
+
* .build();
|
|
191
|
+
*
|
|
192
|
+
* // Use tagged views in procedures
|
|
193
|
+
* procedure().resource(UserSchema.authenticated).query(handler);
|
|
97
194
|
*
|
|
98
|
-
*
|
|
195
|
+
* // Or in handlers
|
|
196
|
+
* resource(data, UserSchema.authenticated);
|
|
197
|
+
* ```
|
|
99
198
|
*/
|
|
100
199
|
build() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
200
|
+
const fields = [...this._fields];
|
|
201
|
+
const base = { fields };
|
|
202
|
+
return Object.assign(base, {
|
|
203
|
+
public: Object.assign({ fields }, { _level: 'public' }),
|
|
204
|
+
authenticated: Object.assign({ fields }, { _level: 'authenticated' }),
|
|
205
|
+
admin: Object.assign({ fields }, { _level: 'admin' }),
|
|
206
|
+
});
|
|
104
207
|
}
|
|
105
208
|
}
|
|
106
209
|
// ============================================================================
|
package/dist/resource/tags.d.ts
CHANGED
|
@@ -16,6 +16,20 @@
|
|
|
16
16
|
* automatic resource projection in the procedure builder.
|
|
17
17
|
*/
|
|
18
18
|
export type AccessLevel = 'public' | 'authenticated' | 'admin';
|
|
19
|
+
/**
|
|
20
|
+
* Maps an AccessLevel string to its corresponding phantom ContextTag
|
|
21
|
+
*
|
|
22
|
+
* Used to bridge the runtime level declarations (e.g., `UserSchema.authenticated`)
|
|
23
|
+
* to the compile-time phantom type system for output type computation.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* type Tag = LevelToTag<'authenticated'>; // typeof AUTHENTICATED
|
|
28
|
+
* type Tag = LevelToTag<'admin'>; // typeof ADMIN
|
|
29
|
+
* type Tag = LevelToTag<'public'>; // typeof ANONYMOUS
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export type LevelToTag<TLevel extends AccessLevel> = TLevel extends 'admin' ? typeof ADMIN : TLevel extends 'authenticated' ? typeof AUTHENTICATED : typeof ANONYMOUS;
|
|
19
33
|
/**
|
|
20
34
|
* Phantom symbol for anonymous (unauthenticated) context
|
|
21
35
|
* @internal Compile-time only - never used at runtime
|
package/dist/types.d.ts
CHANGED
|
@@ -343,6 +343,15 @@ export interface CompiledProcedure<TInput = unknown, TOutput = unknown, TContext
|
|
|
343
343
|
* @internal
|
|
344
344
|
*/
|
|
345
345
|
readonly _resourceSchema?: any;
|
|
346
|
+
/**
|
|
347
|
+
* Explicit resource projection level from tagged schema
|
|
348
|
+
*
|
|
349
|
+
* Set when using `.resource(UserSchema.authenticated)` etc.
|
|
350
|
+
* Takes precedence over guard-derived access level.
|
|
351
|
+
*
|
|
352
|
+
* @internal
|
|
353
|
+
*/
|
|
354
|
+
readonly _resourceLevel?: 'public' | 'authenticated' | 'admin';
|
|
346
355
|
}
|
|
347
356
|
/**
|
|
348
357
|
* Record of named procedures
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veloxts/router",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.99",
|
|
4
4
|
"description": "Procedure definitions with tRPC and REST routing for VeloxTS framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"@trpc/server": "11.9.0",
|
|
41
41
|
"fastify": "5.7.2",
|
|
42
42
|
"zod-to-json-schema": "3.25.1",
|
|
43
|
-
"@veloxts/
|
|
44
|
-
"@veloxts/
|
|
43
|
+
"@veloxts/core": "0.6.99",
|
|
44
|
+
"@veloxts/validation": "0.6.99"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@vitest/coverage-v8": "4.0.18",
|