@veloxts/router 0.7.5 → 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.
@@ -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>;
@@ -0,0 +1,109 @@
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
+ // Default Levels
12
+ // ============================================================================
13
+ /**
14
+ * The built-in 3-level hierarchy used when `resourceSchema()` is called
15
+ * without arguments.
16
+ */
17
+ export const DEFAULT_LEVELS = ['public', 'authenticated', 'admin'];
18
+ export function defineAccessLevels(levels, groups) {
19
+ // Validation: at least 2 levels
20
+ if (levels.length < 2) {
21
+ throw new Error('defineAccessLevels requires at least 2 levels');
22
+ }
23
+ // Validation: no duplicate levels
24
+ const seen = new Set();
25
+ for (const level of levels) {
26
+ if (seen.has(level)) {
27
+ throw new Error(`Duplicate level: "${level}"`);
28
+ }
29
+ seen.add(level);
30
+ }
31
+ const levelSet = new Set(levels);
32
+ const resolvedGroups = new Map();
33
+ const safeGroups = (groups ?? {});
34
+ if (groups) {
35
+ for (const [name, ref] of Object.entries(groups)) {
36
+ // Group names must not collide with level names
37
+ if (levelSet.has(name)) {
38
+ throw new Error(`Group name "${name}" collides with a level name. ` +
39
+ 'Group names and level names must be distinct.');
40
+ }
41
+ if (ref === '*') {
42
+ resolvedGroups.set(name, levelSet);
43
+ }
44
+ else if (Array.isArray(ref)) {
45
+ // Validate that all referenced levels exist
46
+ for (const member of ref) {
47
+ if (!levelSet.has(member)) {
48
+ throw new Error(`Group "${name}" references unknown level "${member}". ` +
49
+ `Valid levels: ${levels.join(', ')}`);
50
+ }
51
+ }
52
+ if (ref.length === 0) {
53
+ throw new Error(`Group "${name}" must contain at least one level`);
54
+ }
55
+ resolvedGroups.set(name, new Set(ref));
56
+ }
57
+ }
58
+ }
59
+ return {
60
+ levels,
61
+ groups: safeGroups,
62
+ resolve(ref) {
63
+ const resolved = resolvedGroups.get(ref);
64
+ if (!resolved) {
65
+ throw new Error(`Unknown group: "${ref}"`);
66
+ }
67
+ return resolved;
68
+ },
69
+ allLevels() {
70
+ return levelSet;
71
+ },
72
+ };
73
+ }
74
+ // ============================================================================
75
+ // Default Config
76
+ // ============================================================================
77
+ /**
78
+ * Pre-built config for the default 3-level system.
79
+ *
80
+ * Used internally by the default `resourceSchema()` builder.
81
+ * The default levels use a hierarchical model where higher levels
82
+ * include all fields visible to lower levels.
83
+ */
84
+ export const DEFAULT_ACCESS_LEVELS = defineAccessLevels(DEFAULT_LEVELS);
85
+ // ============================================================================
86
+ // Runtime Helpers
87
+ // ============================================================================
88
+ /**
89
+ * Converts a default hierarchical level to the set of levels that can see
90
+ * a field at that visibility.
91
+ *
92
+ * - `'public'` → `Set(['public', 'authenticated', 'admin'])`
93
+ * - `'authenticated'` → `Set(['authenticated', 'admin'])`
94
+ * - `'admin'` → `Set(['admin'])`
95
+ *
96
+ * @internal
97
+ */
98
+ export function defaultLevelToSet(level) {
99
+ switch (level) {
100
+ case 'public':
101
+ return new Set(['public', 'authenticated', 'admin']);
102
+ case 'authenticated':
103
+ return new Set(['authenticated', 'admin']);
104
+ case 'admin':
105
+ return new Set(['admin']);
106
+ default:
107
+ return new Set([level]);
108
+ }
109
+ }