@veloxts/router 0.7.5 → 0.7.7

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.
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import type { BaseContext } from '@veloxts/core';
13
13
  import type { ZodType } from 'zod';
14
- import type { OutputForTag, ResourceSchema, TaggedResourceSchema } from '../resource/index.js';
14
+ import type { FilterFieldsByLevel, OutputForTag, ResourceSchema, TaggedResourceSchema } from '../resource/index.js';
15
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
  /**
@@ -94,10 +94,13 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
94
94
  */
95
95
  input<TSchema extends ValidSchema>(schema: TSchema): ProcedureBuilder<InferSchemaOutput<TSchema>, TOutput, TContext>;
96
96
  /**
97
- * Defines the output validation schema for the procedure
97
+ * Defines the output validation schema (Zod)
98
98
  *
99
- * The output type is automatically inferred from the Zod schema.
100
- * The handler return type will be validated against this schema.
99
+ * Sets a Zod schema that validates the handler's return value.
100
+ * All callers receive the same fields.
101
+ *
102
+ * For field-level visibility (different fields per access level),
103
+ * use `.expose()` with a resource schema instead.
101
104
  *
102
105
  * @template TSchema - The Zod schema type
103
106
  * @param schema - Zod schema for output validation
@@ -106,14 +109,39 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
106
109
  * @example
107
110
  * ```typescript
108
111
  * procedure()
109
- * .output(z.object({
110
- * id: z.string(),
111
- * name: z.string(),
112
- * }))
113
- * // handler must return { id: string; name: string }
112
+ * .output(z.object({ id: z.string(), name: z.string() }))
113
+ * .query(handler) // handler must return { id: string; name: string }
114
114
  * ```
115
115
  */
116
116
  output<TSchema extends ValidSchema>(schema: TSchema): ProcedureBuilder<TInput, InferSchemaOutput<TSchema>, TContext>;
117
+ /**
118
+ * Sets field-level visibility via a resource schema
119
+ *
120
+ * Accepts two resource schema variants:
121
+ * 1. **Tagged resource schema** (e.g., `UserSchema.authenticated`) — explicit field projection by access level
122
+ * 2. **Plain resource schema** (e.g., `UserSchema`) — context-derived field projection from `guardNarrow`
123
+ *
124
+ * @template TSchema - The resource schema type
125
+ * @param schema - Resource schema for field projection
126
+ * @returns New builder with updated output type
127
+ *
128
+ * @example Tagged resource schema — explicit projection level
129
+ * ```typescript
130
+ * procedure()
131
+ * .guard(authenticated)
132
+ * .expose(UserSchema.authenticated) // returns { id, name, email }
133
+ * .query(handler)
134
+ * ```
135
+ *
136
+ * @example Plain resource schema — derives level from guardNarrow
137
+ * ```typescript
138
+ * procedure()
139
+ * .guardNarrow(authenticatedNarrow)
140
+ * .expose(UserSchema) // auto-projects based on guard's accessLevel
141
+ * .query(handler)
142
+ * ```
143
+ */
144
+ expose<TSchema extends ResourceSchema>(schema: TSchema): ProcedureBuilder<TInput, TSchema extends TaggedResourceSchema<infer TFields, infer TLevel> ? TLevel extends 'admin' | 'authenticated' | 'public' ? OutputForTag<ResourceSchema<TFields>, LevelToTag<TLevel>> : FilterFieldsByLevel<TFields, TLevel> : TContext extends TaggedContext<infer TTag> ? TTag extends ContextTag ? OutputForTag<TSchema, TTag> : OutputForTag<TSchema, ExtractTag<TContext>> : OutputForTag<TSchema, ExtractTag<TContext>>, TContext>;
117
145
  /**
118
146
  * Adds middleware to the procedure chain
119
147
  *
@@ -255,6 +283,26 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
255
283
  * ```
256
284
  */
257
285
  rest(config: RestRouteOverride): ProcedureBuilder<TInput, TOutput, TContext>;
286
+ /**
287
+ * Configures the procedure as a webhook endpoint
288
+ *
289
+ * Sugar for `.rest({ method: 'POST', path })` with a webhook flag.
290
+ * Webhook procedures expect raw body access for signature verification.
291
+ *
292
+ * @param path - The webhook endpoint path (e.g., '/webhooks/stripe')
293
+ * @returns New builder with webhook configuration
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * procedure()
298
+ * .webhook('/webhooks/stripe')
299
+ * .mutation(async ({ input, ctx }) => {
300
+ * const isValid = verifySignature(ctx.request.rawBody, ctx.request.headers['stripe-signature']);
301
+ * // ...
302
+ * })
303
+ * ```
304
+ */
305
+ webhook(path: string): ProcedureBuilder<TInput, TOutput, TContext>;
258
306
  /**
259
307
  * Marks the procedure as deprecated
260
308
  *
@@ -377,28 +425,17 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
377
425
  */
378
426
  mutation(handler: ProcedureHandler<TInput, TOutput, TContext>): CompiledProcedure<TInput, TOutput, TContext, 'mutation'>;
379
427
  /**
380
- * Sets the output type based on a resource schema
428
+ * @deprecated Use `.expose()` instead. `.resource()` will be removed in v1.0.
381
429
  *
382
- * Accepts either a tagged schema (e.g., `UserSchema.authenticated`) for
383
- * explicit auto-projection, or a plain schema for backward compatibility.
430
+ * Sets field-level visibility via a resource schema.
384
431
  *
385
- * When a tagged schema is used, the output type is computed from the
386
- * tag's access level. When a plain schema is used, the output type is
387
- * derived from the context's phantom tag (set by `guardNarrow`).
388
- *
389
- * @example
432
+ * @example Migration
390
433
  * ```typescript
391
- * // Tagged schema — explicit projection level (recommended)
392
- * procedure()
393
- * .guard(authenticated)
394
- * .resource(UserSchema.authenticated)
395
- * .query(async ({ ctx }) => ctx.db.user.findUnique(...));
434
+ * // Before
435
+ * procedure().resource(UserSchema.authenticated).query(handler)
396
436
  *
397
- * // Plain schema — derives level from guardNarrow or defaults to public
398
- * procedure()
399
- * .guardNarrow(authenticatedNarrow)
400
- * .resource(UserSchema)
401
- * .query(async ({ ctx }) => ctx.db.user.findUnique(...));
437
+ * // After
438
+ * procedure().expose(UserSchema.authenticated).query(handler)
402
439
  * ```
403
440
  */
404
441
  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>;
@@ -417,7 +454,7 @@ export interface BuilderRuntimeState {
417
454
  /** Resource schema for context-dependent output */
418
455
  resourceSchema?: ResourceSchema;
419
456
  /** Explicit resource level from tagged schema (e.g., UserSchema.authenticated) */
420
- resourceLevel?: 'public' | 'authenticated' | 'admin';
457
+ resourceLevel?: string;
421
458
  /** Middleware chain */
422
459
  middlewares: MiddlewareFunction<unknown, BaseContext, BaseContext, unknown>[];
423
460
  /** Guards to execute before handler */
@@ -432,6 +469,8 @@ export interface BuilderRuntimeState {
432
469
  deprecated?: boolean;
433
470
  /** Deprecation message */
434
471
  deprecationMessage?: string;
472
+ /** Whether this procedure is a webhook endpoint (metadata marker) */
473
+ isWebhook?: boolean;
435
474
  }
436
475
  /**
437
476
  * Type for the procedures object passed to defineProcedures
@@ -34,7 +34,7 @@
34
34
  * .input(z.object({ id: z.string() }))
35
35
  * .query(async ({ input, ctx }) => {
36
36
  * const user = await ctx.db.user.findUnique({ where: { id: input.id } });
37
- * return resource(user, UserSchema).forAnonymous();
37
+ * return resource(user, UserSchema).forPublic();
38
38
  * }),
39
39
  *
40
40
  * // Authenticated endpoint → returns { id, name, email }
@@ -60,9 +60,11 @@
60
60
  * @module resource
61
61
  */
62
62
  export { Resource, ResourceCollection, resource, resourceCollection } from './instance.js';
63
- export type { AdminOutput, AnonymousOutput, AuthenticatedOutput, BuilderField, OutputForLevel, OutputForTag, RelationField, ResourceField, ResourceSchema, ResourceSchemaWithViews, RuntimeField, TaggedResourceSchema, } from './schema.js';
63
+ export type { AccessLevelConfig } from './levels.js';
64
+ export { defineAccessLevels } from './levels.js';
65
+ export type { AdminOutput, AnonymousOutput, AuthenticatedOutput, BuilderField, CustomResourceSchemaWithViews, CustomSchemaBuilder, FilterFieldsByLevel, OutputForLevel, OutputForTag, PublicOutput, RelationField, ResourceField, ResourceSchema, ResourceSchemaWithViews, RuntimeField, TaggedResourceSchema, } from './schema.js';
64
66
  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
- export type { AdminTaggedContext, AnonymousTaggedContext, AnyResourceOutput, AuthenticatedTaggedContext, IfAdmin, IfAuthenticated, InferResourceData, InferResourceOutput, } from './types.js';
67
+ export type { AccessLevel, ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag, ExtractTag, HasTag, LevelToTag, PUBLIC, TaggedContext, TagToLevel, WithTag, } from './tags.js';
68
+ export type { AdminTaggedContext, AnonymousTaggedContext, AnyResourceOutput, AuthenticatedTaggedContext, IfAdmin, IfAuthenticated, InferResourceData, InferResourceOutput, PublicTaggedContext, } from './types.js';
67
69
  export type { IsVisibleToTag, VisibilityLevel } from './visibility.js';
68
- export { getAccessibleLevels, getVisibilityForTag, isVisibleAtLevel } from './visibility.js';
70
+ export { getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isVisibleAtLevel, } from './visibility.js';
@@ -34,7 +34,7 @@
34
34
  * .input(z.object({ id: z.string() }))
35
35
  * .query(async ({ input, ctx }) => {
36
36
  * const user = await ctx.db.user.findUnique({ where: { id: input.id } });
37
- * return resource(user, UserSchema).forAnonymous();
37
+ * return resource(user, UserSchema).forPublic();
38
38
  * }),
39
39
  *
40
40
  * // Authenticated endpoint → returns { id, name, email }
@@ -64,7 +64,8 @@
64
64
  // ============================================================================
65
65
  // Resource instances
66
66
  export { Resource, ResourceCollection, resource, resourceCollection } from './instance.js';
67
+ export { defineAccessLevels } from './levels.js';
67
68
  // Schema builder
68
69
  export { isResourceSchema, isTaggedResourceSchema, ResourceSchemaBuilder, resourceSchema, } from './schema.js';
69
70
  // Visibility
70
- export { getAccessibleLevels, getVisibilityForTag, isVisibleAtLevel } from './visibility.js';
71
+ export { getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isVisibleAtLevel, } from './visibility.js';
@@ -6,8 +6,8 @@
6
6
  *
7
7
  * @module resource/instance
8
8
  */
9
- import type { OutputForLevel, OutputForTag, ResourceSchema, TaggedResourceSchema } from './schema.js';
10
- import type { ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag, ExtractTag, TaggedContext } from './tags.js';
9
+ import type { FilterFieldsByLevel, OutputForLevel, OutputForTag, ResourceSchema, TaggedResourceSchema } from './schema.js';
10
+ import type { ADMIN, AUTHENTICATED, ContextTag, ExtractTag, PUBLIC, TaggedContext } from './tags.js';
11
11
  /**
12
12
  * A resource instance that can project data based on access level
13
13
  *
@@ -23,7 +23,7 @@ import type { ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag, ExtractTag, TaggedCon
23
23
  * const resource = new Resource(user, UserSchema);
24
24
  *
25
25
  * // Returns only public fields: { id, name }
26
- * const publicData = resource.forAnonymous();
26
+ * const publicData = resource.forPublic();
27
27
  *
28
28
  * // Returns public + authenticated fields: { id, name, email }
29
29
  * const authData = resource.forAuthenticated();
@@ -37,11 +37,13 @@ export declare class Resource<TSchema extends ResourceSchema> {
37
37
  private readonly _schema;
38
38
  constructor(data: Record<string, unknown>, schema: TSchema);
39
39
  /**
40
- * Projects data for anonymous (unauthenticated) access
40
+ * Projects data for public (unauthenticated) access
41
41
  *
42
42
  * Returns only fields marked as 'public'.
43
43
  */
44
- forAnonymous(): OutputForTag<TSchema, typeof ANONYMOUS>;
44
+ forPublic(): OutputForTag<TSchema, typeof PUBLIC>;
45
+ /** @deprecated Use forPublic() */
46
+ forAnonymous(): OutputForTag<TSchema, typeof PUBLIC>;
45
47
  /**
46
48
  * Projects data for authenticated user access
47
49
  *
@@ -54,6 +56,22 @@ export declare class Resource<TSchema extends ResourceSchema> {
54
56
  * Returns all fields (public, authenticated, and admin).
55
57
  */
56
58
  forAdmin(): OutputForTag<TSchema, typeof ADMIN>;
59
+ /**
60
+ * Projects data for an explicit access level string
61
+ *
62
+ * Works with both default levels ('public', 'authenticated', 'admin')
63
+ * and custom levels defined via `defineAccessLevels()`.
64
+ *
65
+ * @param level - The access level to project for
66
+ * @returns Object with only the fields visible to the given level
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * // Custom level projection
71
+ * const data = resource.forLevel('reviewer');
72
+ * ```
73
+ */
74
+ forLevel<TLevel extends string>(level: TLevel): TSchema extends ResourceSchema<infer TFields> ? FilterFieldsByLevel<TFields, TLevel> : Record<string, unknown>;
57
75
  /**
58
76
  * Projects data based on a tagged context
59
77
  *
@@ -70,16 +88,6 @@ export declare class Resource<TSchema extends ResourceSchema> {
70
88
  * ```
71
89
  */
72
90
  for<TContext extends TaggedContext<ContextTag>>(ctx: TContext): OutputForTag<TSchema, ExtractTag<TContext>>;
73
- /**
74
- * Projects data for an explicit visibility level
75
- *
76
- * Delegates to the standalone `projectData()` function which supports
77
- * recursive projection of nested relations.
78
- *
79
- * @param level - The visibility level to project for
80
- * @returns Object with only the visible fields (null prototype)
81
- */
82
- private _project;
83
91
  /**
84
92
  * Infers the visibility level from a context object
85
93
  *
@@ -104,7 +112,7 @@ export declare class Resource<TSchema extends ResourceSchema> {
104
112
  * const collection = new ResourceCollection(users, UserSchema);
105
113
  *
106
114
  * // Returns array of public views
107
- * const publicList = collection.forAnonymous();
115
+ * const publicList = collection.forPublic();
108
116
  *
109
117
  * // Returns array with authenticated fields
110
118
  * const authList = collection.forAuthenticated();
@@ -115,9 +123,11 @@ export declare class ResourceCollection<TSchema extends ResourceSchema> {
115
123
  private readonly _schema;
116
124
  constructor(items: Array<Record<string, unknown>>, schema: TSchema);
117
125
  /**
118
- * Projects all items for anonymous access
126
+ * Projects all items for public (unauthenticated) access
119
127
  */
120
- forAnonymous(): Array<OutputForTag<TSchema, typeof ANONYMOUS>>;
128
+ forPublic(): Array<OutputForTag<TSchema, typeof PUBLIC>>;
129
+ /** @deprecated Use forPublic() */
130
+ forAnonymous(): Array<OutputForTag<TSchema, typeof PUBLIC>>;
121
131
  /**
122
132
  * Projects all items for authenticated user access
123
133
  */
@@ -126,6 +136,14 @@ export declare class ResourceCollection<TSchema extends ResourceSchema> {
126
136
  * Projects all items for admin access
127
137
  */
128
138
  forAdmin(): Array<OutputForTag<TSchema, typeof ADMIN>>;
139
+ /**
140
+ * Projects all items for an explicit access level string
141
+ *
142
+ * Works with both default and custom access levels.
143
+ *
144
+ * @param level - The access level to project for
145
+ */
146
+ forLevel<TLevel extends string>(level: TLevel): TSchema extends ResourceSchema<infer TFields> ? Array<FilterFieldsByLevel<TFields, TLevel>> : Array<Record<string, unknown>>;
129
147
  /**
130
148
  * Projects all items based on a tagged context
131
149
  */
@@ -144,8 +162,8 @@ export declare class ResourceCollection<TSchema extends ResourceSchema> {
144
162
  *
145
163
  * When called with a tagged schema (e.g., `UserSchema.authenticated`),
146
164
  * returns the projected data directly. When called with an untagged schema,
147
- * returns a Resource instance with `.forAnonymous()`, `.forAuthenticated()`,
148
- * `.forAdmin()` methods.
165
+ * returns a Resource instance with `.forPublic()`, `.forAuthenticated()`,
166
+ * `.forAdmin()`, `.forLevel()` methods.
149
167
  *
150
168
  * @param data - The raw data object
151
169
  * @param schema - Resource schema (tagged for direct projection, untagged for Resource instance)
@@ -7,7 +7,7 @@
7
7
  * @module resource/instance
8
8
  */
9
9
  import { isTaggedResourceSchema } from './schema.js';
10
- import { isVisibleAtLevel } from './visibility.js';
10
+ import { isFieldVisibleToLevel } from './visibility.js';
11
11
  // ============================================================================
12
12
  // Security Constants
13
13
  // ============================================================================
@@ -38,7 +38,7 @@ const MAX_PROJECTION_DEPTH = 10;
38
38
  * @internal
39
39
  * @param data - The raw data object
40
40
  * @param schema - The resource schema to project against
41
- * @param level - The visibility level to project for
41
+ * @param level - The access level to project for (any string)
42
42
  * @param depth - Current recursion depth (defaults to 0)
43
43
  * @param visited - Set of already-visited data objects for cycle detection
44
44
  * @returns Projected object with null prototype
@@ -54,7 +54,7 @@ function projectData(data, schema, level, depth = 0, visited) {
54
54
  seen.add(data);
55
55
  const result = Object.create(null);
56
56
  for (const field of schema.fields) {
57
- if (!isVisibleAtLevel(field.visibility, level))
57
+ if (!isFieldVisibleToLevel(field, level))
58
58
  continue;
59
59
  if (DANGEROUS_PROPERTIES.has(field.name))
60
60
  continue;
@@ -107,7 +107,7 @@ function projectData(data, schema, level, depth = 0, visited) {
107
107
  * const resource = new Resource(user, UserSchema);
108
108
  *
109
109
  * // Returns only public fields: { id, name }
110
- * const publicData = resource.forAnonymous();
110
+ * const publicData = resource.forPublic();
111
111
  *
112
112
  * // Returns public + authenticated fields: { id, name, email }
113
113
  * const authData = resource.forAuthenticated();
@@ -124,12 +124,16 @@ export class Resource {
124
124
  this._schema = schema;
125
125
  }
126
126
  /**
127
- * Projects data for anonymous (unauthenticated) access
127
+ * Projects data for public (unauthenticated) access
128
128
  *
129
129
  * Returns only fields marked as 'public'.
130
130
  */
131
+ forPublic() {
132
+ return this.forLevel('public');
133
+ }
134
+ /** @deprecated Use forPublic() */
131
135
  forAnonymous() {
132
- return this._project('public');
136
+ return this.forPublic();
133
137
  }
134
138
  /**
135
139
  * Projects data for authenticated user access
@@ -137,7 +141,7 @@ export class Resource {
137
141
  * Returns fields marked as 'public' or 'authenticated'.
138
142
  */
139
143
  forAuthenticated() {
140
- return this._project('authenticated');
144
+ return this.forLevel('authenticated');
141
145
  }
142
146
  /**
143
147
  * Projects data for admin access
@@ -145,7 +149,25 @@ export class Resource {
145
149
  * Returns all fields (public, authenticated, and admin).
146
150
  */
147
151
  forAdmin() {
148
- return this._project('admin');
152
+ return this.forLevel('admin');
153
+ }
154
+ /**
155
+ * Projects data for an explicit access level string
156
+ *
157
+ * Works with both default levels ('public', 'authenticated', 'admin')
158
+ * and custom levels defined via `defineAccessLevels()`.
159
+ *
160
+ * @param level - The access level to project for
161
+ * @returns Object with only the fields visible to the given level
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * // Custom level projection
166
+ * const data = resource.forLevel('reviewer');
167
+ * ```
168
+ */
169
+ forLevel(level) {
170
+ return projectData(this._data, this._schema, level);
149
171
  }
150
172
  /**
151
173
  * Projects data based on a tagged context
@@ -166,19 +188,7 @@ export class Resource {
166
188
  // At runtime, we need to determine the level from context properties
167
189
  // Since the tag is phantom (doesn't exist at runtime), we use heuristics
168
190
  const level = this._inferLevelFromContext(ctx);
169
- return this._project(level);
170
- }
171
- /**
172
- * Projects data for an explicit visibility level
173
- *
174
- * Delegates to the standalone `projectData()` function which supports
175
- * recursive projection of nested relations.
176
- *
177
- * @param level - The visibility level to project for
178
- * @returns Object with only the visible fields (null prototype)
179
- */
180
- _project(level) {
181
- return projectData(this._data, this._schema, level);
191
+ return this.forLevel(level);
182
192
  }
183
193
  /**
184
194
  * Infers the visibility level from a context object
@@ -232,7 +242,7 @@ export class Resource {
232
242
  * const collection = new ResourceCollection(users, UserSchema);
233
243
  *
234
244
  * // Returns array of public views
235
- * const publicList = collection.forAnonymous();
245
+ * const publicList = collection.forPublic();
236
246
  *
237
247
  * // Returns array with authenticated fields
238
248
  * const authList = collection.forAuthenticated();
@@ -246,10 +256,14 @@ export class ResourceCollection {
246
256
  this._schema = schema;
247
257
  }
248
258
  /**
249
- * Projects all items for anonymous access
259
+ * Projects all items for public (unauthenticated) access
250
260
  */
261
+ forPublic() {
262
+ return this._items.map((item) => new Resource(item, this._schema).forPublic());
263
+ }
264
+ /** @deprecated Use forPublic() */
251
265
  forAnonymous() {
252
- return this._items.map((item) => new Resource(item, this._schema).forAnonymous());
266
+ return this.forPublic();
253
267
  }
254
268
  /**
255
269
  * Projects all items for authenticated user access
@@ -263,6 +277,16 @@ export class ResourceCollection {
263
277
  forAdmin() {
264
278
  return this._items.map((item) => new Resource(item, this._schema).forAdmin());
265
279
  }
280
+ /**
281
+ * Projects all items for an explicit access level string
282
+ *
283
+ * Works with both default and custom access levels.
284
+ *
285
+ * @param level - The access level to project for
286
+ */
287
+ forLevel(level) {
288
+ return this._items.map((item) => new Resource(item, this._schema).forLevel(level));
289
+ }
266
290
  /**
267
291
  * Projects all items based on a tagged context
268
292
  */
@@ -284,31 +308,14 @@ export class ResourceCollection {
284
308
  }
285
309
  export function resource(data, schema) {
286
310
  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
- }
311
+ return new Resource(data, schema).forLevel(schema._level);
296
312
  }
297
313
  return new Resource(data, schema);
298
314
  }
299
315
  export function resourceCollection(items, schema) {
300
316
  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
- });
317
+ const level = schema._level;
318
+ return items.map((item) => new Resource(item, schema).forLevel(level));
312
319
  }
313
320
  return new ResourceCollection(items, schema);
314
321
  }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Access level configuration for custom visibility systems
3
+ *
4
+ * Provides `defineAccessLevels()` for creating custom access level
5
+ * hierarchies with named groups. The default 3-level system
6
+ * (public/authenticated/admin) is a special case of this.
7
+ *
8
+ * @module resource/levels
9
+ */
10
+ /**
11
+ * The built-in 3-level hierarchy used when `resourceSchema()` is called
12
+ * without arguments.
13
+ */
14
+ export declare const DEFAULT_LEVELS: readonly ["public", "authenticated", "admin"];
15
+ export type DefaultLevels = typeof DEFAULT_LEVELS;
16
+ /**
17
+ * Configuration object returned by `defineAccessLevels()`.
18
+ *
19
+ * Holds the defined levels, named groups, and a `resolve()` method
20
+ * that expands a group name to the concrete set of levels it covers.
21
+ *
22
+ * @template TLevels - Tuple of level name literals
23
+ * @template TGroups - Record mapping group names to `'*'` or level arrays
24
+ */
25
+ export interface AccessLevelConfig<TLevels extends readonly string[] = readonly string[], TGroups extends Record<string, '*' | readonly string[]> = Record<string, never>> {
26
+ readonly levels: TLevels;
27
+ readonly groups: TGroups;
28
+ /** Resolves a group name to the concrete set of levels */
29
+ resolve(ref: keyof TGroups & string): ReadonlySet<string>;
30
+ /** Returns the set containing all defined levels */
31
+ allLevels(): ReadonlySet<string>;
32
+ }
33
+ /**
34
+ * Defines custom access levels and optional named groups.
35
+ *
36
+ * Groups become fluent builder methods on `resourceSchema(config)`.
37
+ * The `'*'` wildcard resolves to all defined levels.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const access = defineAccessLevels(
42
+ * ['public', 'reviewer', 'authenticated', 'moderator', 'admin'],
43
+ * {
44
+ * everyone: '*',
45
+ * internal: ['reviewer', 'moderator', 'admin'],
46
+ * staff: ['moderator', 'admin'],
47
+ * }
48
+ * );
49
+ * ```
50
+ */
51
+ export declare function defineAccessLevels<const TLevels extends readonly [string, ...string[]]>(levels: TLevels): AccessLevelConfig<TLevels, Record<string, never>>;
52
+ export declare function defineAccessLevels<const TLevels extends readonly [string, ...string[]], const TGroups extends Record<string, '*' | readonly NoInfer<TLevels[number]>[]>>(levels: TLevels, groups: TGroups): AccessLevelConfig<TLevels, TGroups>;
53
+ /**
54
+ * Pre-built config for the default 3-level system.
55
+ *
56
+ * Used internally by the default `resourceSchema()` builder.
57
+ * The default levels use a hierarchical model where higher levels
58
+ * include all fields visible to lower levels.
59
+ */
60
+ export declare const DEFAULT_ACCESS_LEVELS: AccessLevelConfig<DefaultLevels, Record<string, never>>;
61
+ /**
62
+ * Converts a default hierarchical level to the set of levels that can see
63
+ * a field at that visibility.
64
+ *
65
+ * - `'public'` → `Set(['public', 'authenticated', 'admin'])`
66
+ * - `'authenticated'` → `Set(['authenticated', 'admin'])`
67
+ * - `'admin'` → `Set(['admin'])`
68
+ *
69
+ * @internal
70
+ */
71
+ export declare function defaultLevelToSet(level: string): ReadonlySet<string>;