@veloxts/router 0.7.4 → 0.7.6

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.
@@ -5,20 +5,12 @@
5
5
  * field-level visibility controls. The builder tracks field types
6
6
  * at compile time to enable type-safe projections.
7
7
  *
8
+ * Supports both the default 3-level system (public/authenticated/admin)
9
+ * and custom access levels defined via `defineAccessLevels()`.
10
+ *
8
11
  * @module resource/schema
9
12
  */
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
- }
13
+ import { defaultLevelToSet } from './levels.js';
22
14
  // ============================================================================
23
15
  // Schema Builder
24
16
  // ============================================================================
@@ -53,66 +45,43 @@ export class ResourceSchemaBuilder {
53
45
  }
54
46
  /**
55
47
  * Adds a public field (visible to everyone)
56
- *
57
- * @param name - Field name
58
- * @param schema - Zod schema for the field
59
- * @returns New builder with the field added
60
48
  */
61
49
  public(name, schema) {
62
50
  return new ResourceSchemaBuilder([
63
51
  ...this._fields,
64
- { name, schema, visibility: 'public' },
52
+ {
53
+ name,
54
+ schema,
55
+ visibility: 'public',
56
+ visibleTo: new Set(['public', 'authenticated', 'admin']),
57
+ },
65
58
  ]);
66
59
  }
67
60
  /**
68
61
  * Adds an authenticated field (visible to authenticated users and admins)
69
- *
70
- * @param name - Field name
71
- * @param schema - Zod schema for the field
72
- * @returns New builder with the field added
73
62
  */
74
63
  authenticated(name, schema) {
75
64
  return new ResourceSchemaBuilder([
76
65
  ...this._fields,
77
- { name, schema, visibility: 'authenticated' },
66
+ {
67
+ name,
68
+ schema,
69
+ visibility: 'authenticated',
70
+ visibleTo: new Set(['authenticated', 'admin']),
71
+ },
78
72
  ]);
79
73
  }
80
74
  /**
81
75
  * Adds an admin field (visible only to admins)
82
- *
83
- * @param name - Field name
84
- * @param schema - Zod schema for the field
85
- * @returns New builder with the field added
86
76
  */
87
77
  admin(name, schema) {
88
78
  return new ResourceSchemaBuilder([
89
79
  ...this._fields,
90
- { name, schema, visibility: 'admin' },
80
+ { name, schema, visibility: 'admin', visibleTo: new Set(['admin']) },
91
81
  ]);
92
82
  }
93
83
  /**
94
84
  * 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
85
  */
117
86
  hasOne(name, nestedSchema, visibility) {
118
87
  return new ResourceSchemaBuilder([
@@ -120,6 +89,7 @@ export class ResourceSchemaBuilder {
120
89
  {
121
90
  name,
122
91
  visibility,
92
+ visibleTo: defaultLevelToSet(visibility),
123
93
  nestedSchema: nestedSchema,
124
94
  cardinality: 'one',
125
95
  },
@@ -127,27 +97,6 @@ export class ResourceSchemaBuilder {
127
97
  }
128
98
  /**
129
99
  * 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
100
  */
152
101
  hasMany(name, nestedSchema, visibility) {
153
102
  return new ResourceSchemaBuilder([
@@ -155,6 +104,7 @@ export class ResourceSchemaBuilder {
155
104
  {
156
105
  name,
157
106
  visibility,
107
+ visibleTo: defaultLevelToSet(visibility),
158
108
  nestedSchema: nestedSchema,
159
109
  cardinality: 'many',
160
110
  },
@@ -162,39 +112,15 @@ export class ResourceSchemaBuilder {
162
112
  }
163
113
  /**
164
114
  * Adds a field with explicit visibility level
165
- *
166
- * @param name - Field name
167
- * @param schema - Zod schema for the field
168
- * @param visibility - Visibility level
169
- * @returns New builder with the field added
170
115
  */
171
116
  field(name, schema, visibility) {
172
117
  return new ResourceSchemaBuilder([
173
118
  ...this._fields,
174
- { name, schema, visibility },
119
+ { name, schema, visibility, visibleTo: defaultLevelToSet(visibility) },
175
120
  ]);
176
121
  }
177
122
  /**
178
123
  * 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);
194
- *
195
- * // Or in handlers
196
- * resource(data, UserSchema.authenticated);
197
- * ```
198
124
  */
199
125
  build() {
200
126
  const fields = [...this._fields];
@@ -206,44 +132,126 @@ export class ResourceSchemaBuilder {
206
132
  });
207
133
  }
208
134
  }
209
- // ============================================================================
210
- // Factory Function
211
- // ============================================================================
212
135
  /**
213
- * Creates a new resource schema builder
214
- *
215
- * This is the primary entry point for defining resource schemas with
216
- * field-level visibility controls.
217
- *
218
- * @returns New empty schema builder
219
- *
220
- * @example
221
- * ```typescript
222
- * import { z } from 'zod';
223
- * import { resourceSchema } from '@veloxts/router';
136
+ * Resolves a visibility reference (group name, level name, or array) to a Set.
137
+ * Group names are resolved via config, level names become single-element sets,
138
+ * and arrays are used directly.
224
139
  *
225
- * const UserSchema = resourceSchema()
226
- * .public('id', z.string().uuid())
227
- * .public('name', z.string())
228
- * .public('avatarUrl', z.string().url().nullable())
229
- * .authenticated('email', z.string().email())
230
- * .authenticated('createdAt', z.date())
231
- * .admin('internalNotes', z.string().nullable())
232
- * .admin('lastLoginIp', z.string().nullable())
233
- * .build();
140
+ * @internal
141
+ */
142
+ function resolveVisibilityRef(visibility, config, groupNames, levelSet) {
143
+ if (typeof visibility !== 'string') {
144
+ return new Set(visibility);
145
+ }
146
+ if (groupNames.has(visibility)) {
147
+ return config.resolve(visibility);
148
+ }
149
+ if (levelSet.has(visibility)) {
150
+ return new Set([visibility]);
151
+ }
152
+ throw new Error(`Unknown group or level: "${visibility}"`);
153
+ }
154
+ /**
155
+ * Creates a Proxy-based custom schema builder
234
156
  *
235
- * // Type inference:
236
- * // AnonymousOutput<typeof UserSchema> = { id: string; name: string; avatarUrl: string | null }
237
- * // AuthenticatedOutput<typeof UserSchema> = { id: string; name: string; avatarUrl: string | null; email: string; createdAt: Date }
238
- * // AdminOutput<typeof UserSchema> = { id: string; name: string; avatarUrl: string | null; email: string; createdAt: Date; internalNotes: string | null; lastLoginIp: string | null }
239
- * ```
157
+ * @internal
240
158
  */
241
- export function resourceSchema() {
242
- return ResourceSchemaBuilder.create();
159
+ function createCustomSchemaBuilder(config, fields) {
160
+ const levelSet = config.allLevels();
161
+ const groupNames = new Set(Object.keys(config.groups));
162
+ const base = {
163
+ visibleTo(name, schema, levels) {
164
+ const set = new Set(levels);
165
+ return createCustomSchemaBuilder(config, [
166
+ ...fields,
167
+ { name, schema, visibleTo: set, visibility: levels[0] ?? '' },
168
+ ]);
169
+ },
170
+ hasOne(name, nestedSchema, visibility) {
171
+ const set = resolveVisibilityRef(visibility, config, groupNames, levelSet);
172
+ return createCustomSchemaBuilder(config, [
173
+ ...fields,
174
+ {
175
+ name,
176
+ nestedSchema,
177
+ visibleTo: set,
178
+ visibility: [...set][0] ?? '',
179
+ cardinality: 'one',
180
+ },
181
+ ]);
182
+ },
183
+ hasMany(name, nestedSchema, visibility) {
184
+ const set = resolveVisibilityRef(visibility, config, groupNames, levelSet);
185
+ return createCustomSchemaBuilder(config, [
186
+ ...fields,
187
+ {
188
+ name,
189
+ nestedSchema,
190
+ visibleTo: set,
191
+ visibility: [...set][0] ?? '',
192
+ cardinality: 'many',
193
+ },
194
+ ]);
195
+ },
196
+ build() {
197
+ const frozenFields = [...fields];
198
+ const baseSchema = { fields: frozenFields };
199
+ const result = Object.assign(baseSchema, { _levelConfig: config });
200
+ for (const level of config.levels) {
201
+ result[level] = Object.assign({ fields: frozenFields }, { _level: level });
202
+ }
203
+ return result;
204
+ },
205
+ };
206
+ return new Proxy(base, {
207
+ get(target, prop, receiver) {
208
+ if (typeof prop === 'string') {
209
+ // Check if it's a group name
210
+ if (groupNames.has(prop)) {
211
+ return (name, schema) => {
212
+ const set = config.resolve(prop);
213
+ return createCustomSchemaBuilder(config, [
214
+ ...fields,
215
+ { name, schema, visibleTo: set, visibility: [...set][0] ?? '' },
216
+ ]);
217
+ };
218
+ }
219
+ // Check if it's a level name (not a group)
220
+ if (levelSet.has(prop)) {
221
+ return (name, schema) => {
222
+ const set = new Set([prop]);
223
+ return createCustomSchemaBuilder(config, [
224
+ ...fields,
225
+ { name, schema, visibleTo: set, visibility: prop },
226
+ ]);
227
+ };
228
+ }
229
+ }
230
+ return Reflect.get(target, prop, receiver);
231
+ },
232
+ });
233
+ }
234
+ export function resourceSchema(config) {
235
+ if (!config) {
236
+ return ResourceSchemaBuilder.create();
237
+ }
238
+ return createCustomSchemaBuilder(config, []);
243
239
  }
244
240
  // ============================================================================
245
241
  // Type Guards
246
242
  // ============================================================================
243
+ /**
244
+ * Type guard to check if a schema is a TaggedResourceSchema (has _level)
245
+ *
246
+ * Accepts any string _level to support custom access levels.
247
+ */
248
+ export function isTaggedResourceSchema(value) {
249
+ if (typeof value !== 'object' || value === null) {
250
+ return false;
251
+ }
252
+ const obj = value;
253
+ return Array.isArray(obj.fields) && typeof obj._level === 'string';
254
+ }
247
255
  /**
248
256
  * Type guard to check if a value is a ResourceSchema
249
257
  */
@@ -5,7 +5,7 @@
5
5
  * through the type system without any runtime overhead.
6
6
  *
7
7
  * Additionally provides runtime `__accessLevel` property for auto-projection
8
- * when using the chained `.resource()` method on procedures.
8
+ * when using the chained `.expose()` method on procedures.
9
9
  *
10
10
  * @module resource/tags
11
11
  */
@@ -14,8 +14,12 @@
14
14
  *
15
15
  * These values are set by narrowing guards at runtime and used for
16
16
  * automatic resource projection in the procedure builder.
17
+ *
18
+ * Widened to `string` to support custom access levels defined via
19
+ * `defineAccessLevels()`. The default 3-level system still uses
20
+ * `'public' | 'authenticated' | 'admin'` at the call site.
17
21
  */
18
- export type AccessLevel = 'public' | 'authenticated' | 'admin';
22
+ export type AccessLevel = string;
19
23
  /**
20
24
  * Maps an AccessLevel string to its corresponding phantom ContextTag
21
25
  *
@@ -26,15 +30,31 @@ export type AccessLevel = 'public' | 'authenticated' | 'admin';
26
30
  * ```typescript
27
31
  * type Tag = LevelToTag<'authenticated'>; // typeof AUTHENTICATED
28
32
  * type Tag = LevelToTag<'admin'>; // typeof ADMIN
29
- * type Tag = LevelToTag<'public'>; // typeof ANONYMOUS
33
+ * type Tag = LevelToTag<'public'>; // typeof PUBLIC
34
+ * ```
35
+ */
36
+ export type LevelToTag<TLevel extends string> = TLevel extends 'admin' ? typeof ADMIN : TLevel extends 'authenticated' ? typeof AUTHENTICATED : typeof PUBLIC;
37
+ /**
38
+ * Maps a phantom ContextTag to its corresponding level string
39
+ *
40
+ * Inverse of `LevelToTag`. Used by `FilterFieldsByLevel` to bridge
41
+ * the tag-based system to the set-based visibility model.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * type L1 = TagToLevel<typeof ADMIN>; // 'admin'
46
+ * type L2 = TagToLevel<typeof AUTHENTICATED>; // 'authenticated'
47
+ * type L3 = TagToLevel<typeof PUBLIC>; // 'public'
30
48
  * ```
31
49
  */
32
- export type LevelToTag<TLevel extends AccessLevel> = TLevel extends 'admin' ? typeof ADMIN : TLevel extends 'authenticated' ? typeof AUTHENTICATED : typeof ANONYMOUS;
50
+ export type TagToLevel<TTag extends ContextTag> = TTag extends typeof ADMIN ? 'admin' : TTag extends typeof AUTHENTICATED ? 'authenticated' : 'public';
33
51
  /**
34
- * Phantom symbol for anonymous (unauthenticated) context
52
+ * Phantom symbol for public (unauthenticated) context
35
53
  * @internal Compile-time only - never used at runtime
36
54
  */
37
- export declare const ANONYMOUS: unique symbol;
55
+ export declare const PUBLIC: unique symbol;
56
+ /** @deprecated Use PUBLIC */
57
+ export declare const ANONYMOUS: typeof PUBLIC;
38
58
  /**
39
59
  * Phantom symbol for authenticated user context
40
60
  * @internal Compile-time only - never used at runtime
@@ -50,7 +70,7 @@ export declare const ADMIN: unique symbol;
50
70
  *
51
71
  * Used to constrain generic type parameters that represent access levels.
52
72
  */
53
- export type ContextTag = typeof ANONYMOUS | typeof AUTHENTICATED | typeof ADMIN;
73
+ export type ContextTag = typeof PUBLIC | typeof AUTHENTICATED | typeof ADMIN;
54
74
  /**
55
75
  * Interface for contexts tagged with an access level
56
76
  *
@@ -59,10 +79,10 @@ export type ContextTag = typeof ANONYMOUS | typeof AUTHENTICATED | typeof ADMIN;
59
79
  * without any memory overhead.
60
80
  *
61
81
  * The `__accessLevel` field is a runtime field set by narrowing guards.
62
- * It enables automatic resource projection when using `.resource()` in
82
+ * It enables automatic resource projection when using `.expose()` in
63
83
  * the procedure builder chain.
64
84
  *
65
- * @template TTag - The context tag type (defaults to ANONYMOUS)
85
+ * @template TTag - The context tag type (defaults to PUBLIC)
66
86
  *
67
87
  * @example
68
88
  * ```typescript
@@ -77,7 +97,7 @@ export type ContextTag = typeof ANONYMOUS | typeof AUTHENTICATED | typeof ADMIN;
77
97
  * }
78
98
  * ```
79
99
  */
80
- export interface TaggedContext<TTag extends ContextTag = typeof ANONYMOUS> {
100
+ export interface TaggedContext<TTag extends ContextTag = typeof PUBLIC> {
81
101
  /**
82
102
  * Phantom field for carrying the context tag
83
103
  * @internal Never exists at runtime - purely for type inference
@@ -87,27 +107,27 @@ export interface TaggedContext<TTag extends ContextTag = typeof ANONYMOUS> {
87
107
  * Runtime access level set by narrowing guards
88
108
  *
89
109
  * This field IS present at runtime (unlike __tag) and is used for
90
- * automatic resource projection when using `.resource()` in procedures.
110
+ * automatic resource projection when using `.expose()` in procedures.
91
111
  *
92
112
  * Set automatically by:
93
113
  * - `authenticatedNarrow` → 'authenticated'
94
114
  * - `adminNarrow` → 'admin'
95
115
  * - No guard → 'public' (default)
96
116
  */
97
- __accessLevel?: AccessLevel;
117
+ __accessLevel?: string;
98
118
  }
99
119
  /**
100
120
  * Extracts the tag from a tagged context type
101
121
  *
102
- * Returns ANONYMOUS if the context is not tagged or has no tag.
122
+ * Returns PUBLIC if the context is not tagged or has no tag.
103
123
  *
104
124
  * @example
105
125
  * ```typescript
106
126
  * type Tag1 = ExtractTag<TaggedContext<typeof ADMIN>>; // typeof ADMIN
107
- * type Tag2 = ExtractTag<{ user: User }>; // typeof ANONYMOUS
127
+ * type Tag2 = ExtractTag<{ user: User }>; // typeof PUBLIC
108
128
  * ```
109
129
  */
110
- export type ExtractTag<TContext> = TContext extends TaggedContext<infer TTag> ? TTag : typeof ANONYMOUS;
130
+ export type ExtractTag<TContext> = TContext extends TaggedContext<infer TTag> ? TTag : typeof PUBLIC;
111
131
  /**
112
132
  * Checks if a context has a specific tag
113
133
  *
@@ -5,7 +5,7 @@
5
5
  * through the type system without any runtime overhead.
6
6
  *
7
7
  * Additionally provides runtime `__accessLevel` property for auto-projection
8
- * when using the chained `.resource()` method on procedures.
8
+ * when using the chained `.expose()` method on procedures.
9
9
  *
10
10
  * @module resource/tags
11
11
  */
@@ -6,8 +6,8 @@
6
6
  *
7
7
  * @module resource/types
8
8
  */
9
- import type { AdminOutput, AnonymousOutput, AuthenticatedOutput, OutputForTag, ResourceSchema } from './schema.js';
10
- import type { ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag, ExtractTag, TaggedContext } from './tags.js';
9
+ import type { AdminOutput, AuthenticatedOutput, OutputForTag, PublicOutput, ResourceSchema } from './schema.js';
10
+ import type { ADMIN, AUTHENTICATED, ContextTag, ExtractTag, PUBLIC, TaggedContext } from './tags.js';
11
11
  /**
12
12
  * Infers the full data shape from a resource schema
13
13
  *
@@ -35,12 +35,14 @@ export type InferResourceData<TSchema extends ResourceSchema> = AdminOutput<TSch
35
35
  */
36
36
  export type InferResourceOutput<TSchema extends ResourceSchema, TContext extends TaggedContext<ContextTag>> = OutputForTag<TSchema, ExtractTag<TContext>>;
37
37
  /**
38
- * Type that represents any anonymous-level tagged context
38
+ * Type that represents any public-level (unauthenticated) tagged context
39
39
  *
40
40
  * This is a minimal tagged context type. For auth-specific contexts with
41
41
  * user and auth properties, use the types from @veloxts/auth.
42
42
  */
43
- export type AnonymousTaggedContext = TaggedContext<typeof ANONYMOUS>;
43
+ export type PublicTaggedContext = TaggedContext<typeof PUBLIC>;
44
+ /** @deprecated Use PublicTaggedContext */
45
+ export type AnonymousTaggedContext = PublicTaggedContext;
44
46
  /**
45
47
  * Type that represents any authenticated-level tagged context
46
48
  *
@@ -69,7 +71,7 @@ export type AdminTaggedContext = TaggedContext<typeof ADMIN>;
69
71
  * }
70
72
  * ```
71
73
  */
72
- export type AnyResourceOutput<TSchema extends ResourceSchema> = AnonymousOutput<TSchema> | AuthenticatedOutput<TSchema> | AdminOutput<TSchema>;
74
+ export type AnyResourceOutput<TSchema extends ResourceSchema> = PublicOutput<TSchema> | AuthenticatedOutput<TSchema> | AdminOutput<TSchema>;
73
75
  /**
74
76
  * Returns the output type if the context has at least authenticated access
75
77
  *
@@ -80,7 +82,7 @@ export type AnyResourceOutput<TSchema extends ResourceSchema> = AnonymousOutput<
80
82
  * // If MyContext is anonymous: never
81
83
  * ```
82
84
  */
83
- export type IfAuthenticated<TContext extends TaggedContext<ContextTag>, TSchema extends ResourceSchema> = ExtractTag<TContext> extends typeof ANONYMOUS ? never : OutputForTag<TSchema, ExtractTag<TContext>>;
85
+ export type IfAuthenticated<TContext extends TaggedContext<ContextTag>, TSchema extends ResourceSchema> = ExtractTag<TContext> extends typeof PUBLIC ? never : OutputForTag<TSchema, ExtractTag<TContext>>;
84
86
  /**
85
87
  * Returns the output type if the context has admin access
86
88
  *
@@ -92,5 +94,5 @@ export type IfAuthenticated<TContext extends TaggedContext<ContextTag>, TSchema
92
94
  * ```
93
95
  */
94
96
  export type IfAdmin<TContext extends TaggedContext<ContextTag>, TSchema extends ResourceSchema> = ExtractTag<TContext> extends typeof ADMIN ? AdminOutput<TSchema> : never;
95
- export type { AdminOutput, AnonymousOutput, AuthenticatedOutput, OutputForTag, ResourceSchema, } from './schema.js';
96
- export type { ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag, ExtractTag, HasTag, TaggedContext, WithTag, } from './tags.js';
97
+ export type { AdminOutput, AnonymousOutput, AuthenticatedOutput, OutputForTag, PublicOutput, ResourceSchema, } from './schema.js';
98
+ export type { ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag, ExtractTag, HasTag, PUBLIC, TaggedContext, WithTag, } from './tags.js';
@@ -22,12 +22,12 @@ export type VisibilityLevel = 'public' | 'authenticated' | 'admin';
22
22
  * Implements the visibility hierarchy:
23
23
  * - ADMIN sees: public, authenticated, admin
24
24
  * - AUTHENTICATED sees: public, authenticated
25
- * - ANONYMOUS sees: public only
25
+ * - PUBLIC sees: public only
26
26
  *
27
27
  * @example
28
28
  * ```typescript
29
- * type T1 = IsVisibleToTag<'public', typeof ANONYMOUS>; // true
30
- * type T2 = IsVisibleToTag<'authenticated', typeof ANONYMOUS>; // false
29
+ * type T1 = IsVisibleToTag<'public', typeof PUBLIC>; // true
30
+ * type T2 = IsVisibleToTag<'authenticated', typeof PUBLIC>; // false
31
31
  * type T3 = IsVisibleToTag<'admin', typeof AUTHENTICATED>; // false
32
32
  * type T4 = IsVisibleToTag<'admin', typeof ADMIN>; // true
33
33
  * ```
@@ -70,3 +70,16 @@ export declare function getVisibilityForTag(tagId: 'anonymous' | 'authenticated'
70
70
  * ```
71
71
  */
72
72
  export declare function getAccessibleLevels(level: VisibilityLevel): VisibilityLevel[];
73
+ /**
74
+ * Runtime check: Is a field visible to a given level using set membership?
75
+ *
76
+ * All fields created by `ResourceSchemaBuilder` and `createCustomSchemaBuilder`
77
+ * include a `visibleTo` set, so this always uses set membership.
78
+ *
79
+ * @param field - The runtime field to check (must have `visibleTo` set)
80
+ * @param level - The access level to check visibility for
81
+ * @returns True if the field should be included in the output
82
+ */
83
+ export declare function isFieldVisibleToLevel(field: {
84
+ visibleTo: ReadonlySet<string>;
85
+ }, level: string): boolean;
@@ -79,3 +79,19 @@ export function getAccessibleLevels(level) {
79
79
  }
80
80
  return levels;
81
81
  }
82
+ // ============================================================================
83
+ // Set-Based Visibility (Custom Levels)
84
+ // ============================================================================
85
+ /**
86
+ * Runtime check: Is a field visible to a given level using set membership?
87
+ *
88
+ * All fields created by `ResourceSchemaBuilder` and `createCustomSchemaBuilder`
89
+ * include a `visibleTo` set, so this always uses set membership.
90
+ *
91
+ * @param field - The runtime field to check (must have `visibleTo` set)
92
+ * @param level - The access level to check visibility for
93
+ * @returns True if the field should be included in the output
94
+ */
95
+ export function isFieldVisibleToLevel(field, level) {
96
+ return field.visibleTo.has(level);
97
+ }
package/dist/types.d.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import type { BaseContext } from '@veloxts/core';
10
10
  import type { HttpMethod } from '@veloxts/validation';
11
+ import type { ResourceSchema } from './resource/schema.js';
11
12
  /**
12
13
  * Procedure operation types
13
14
  *
@@ -186,6 +187,21 @@ export interface MiddlewareArgs<TInput, TContext extends BaseContext = BaseConte
186
187
  * @template TOutput - The output type
187
188
  */
188
189
  export type MiddlewareFunction<TInput, TContext extends BaseContext = BaseContext, TNewContext extends BaseContext = TContext, TOutput = unknown> = (args: MiddlewareArgs<TInput, TContext, TNewContext, TOutput>) => Promise<MiddlewareResult<TOutput>>;
190
+ /**
191
+ * Simple middleware type for most use cases.
192
+ *
193
+ * Use this when defining standalone middleware functions. All generics
194
+ * default to sensible values so you only need to specify what you customize.
195
+ *
196
+ * @example
197
+ * ```typescript
198
+ * const logging: Middleware = async ({ ctx, next }) => {
199
+ * console.log(`${ctx.request.method} ${ctx.request.url}`);
200
+ * return next();
201
+ * };
202
+ * ```
203
+ */
204
+ export type Middleware<TContext extends BaseContext = BaseContext> = MiddlewareFunction<unknown, TContext, TContext>;
189
205
  /**
190
206
  * REST route override configuration
191
207
  *
@@ -282,6 +298,13 @@ export interface CompiledProcedure<TInput = unknown, TOutput = unknown, TContext
282
298
  readonly deprecated?: boolean;
283
299
  /** Deprecation message explaining why and what to use instead */
284
300
  readonly deprecationMessage?: string;
301
+ /**
302
+ * Whether this procedure is a webhook endpoint.
303
+ * Set by `.webhook()` builder method. Currently a metadata marker;
304
+ * will be consumed by REST adapter and OpenAPI generator in a future release.
305
+ * @internal
306
+ */
307
+ readonly isWebhook?: boolean;
285
308
  /**
286
309
  * Parent resource configuration for nested routes (single level)
287
310
  *
@@ -326,14 +349,14 @@ export interface CompiledProcedure<TInput = unknown, TOutput = unknown, TContext
326
349
  /**
327
350
  * Resource schema for auto-projection
328
351
  *
329
- * When set via `.resource()`, the procedure executor will automatically
352
+ * When set via `.expose()`, the procedure executor will automatically
330
353
  * project the handler's return value based on `ctx.__accessLevel`.
331
354
  *
332
355
  * This enables the elegant chained API:
333
356
  * ```typescript
334
357
  * procedure()
335
358
  * .guardNarrow(authenticatedNarrow)
336
- * .resource(UserSchema)
359
+ * .expose(UserSchema)
337
360
  * .query(async ({ ctx }) => {
338
361
  * return ctx.db.user.findUnique(...);
339
362
  * // Auto-projected based on __accessLevel
@@ -342,16 +365,16 @@ export interface CompiledProcedure<TInput = unknown, TOutput = unknown, TContext
342
365
  *
343
366
  * @internal
344
367
  */
345
- readonly _resourceSchema?: any;
368
+ readonly _resourceSchema?: ResourceSchema;
346
369
  /**
347
370
  * Explicit resource projection level from tagged schema
348
371
  *
349
- * Set when using `.resource(UserSchema.authenticated)` etc.
372
+ * Set when using `.expose(UserSchema.authenticated)` etc.
350
373
  * Takes precedence over guard-derived access level.
351
374
  *
352
375
  * @internal
353
376
  */
354
- readonly _resourceLevel?: 'public' | 'authenticated' | 'admin';
377
+ readonly _resourceLevel?: string;
355
378
  }
356
379
  /**
357
380
  * Record of named procedures