@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.
- package/CHANGELOG.md +9 -0
- package/GUIDE.md +9 -9
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -2
- package/dist/openapi/generator.js +9 -4
- package/dist/openapi/schema-converter.d.ts +13 -0
- package/dist/openapi/schema-converter.js +51 -0
- package/dist/procedure/builder.js +33 -21
- package/dist/procedure/types.d.ts +67 -28
- package/dist/resource/index.d.ts +7 -5
- package/dist/resource/index.js +3 -2
- package/dist/resource/instance.d.ts +38 -20
- package/dist/resource/instance.js +51 -44
- package/dist/resource/levels.d.ts +71 -0
- package/dist/resource/levels.js +109 -0
- package/dist/resource/schema.d.ts +151 -159
- package/dist/resource/schema.js +132 -124
- package/dist/resource/tags.d.ts +35 -15
- package/dist/resource/tags.js +1 -1
- package/dist/resource/types.d.ts +10 -8
- package/dist/resource/visibility.d.ts +16 -3
- package/dist/resource/visibility.js +16 -0
- package/dist/types.d.ts +28 -5
- package/package.json +3 -3
|
@@ -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,
|
|
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.
|
|
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
|
|
40
|
+
* Projects data for public (unauthenticated) access
|
|
41
41
|
*
|
|
42
42
|
* Returns only fields marked as 'public'.
|
|
43
43
|
*/
|
|
44
|
-
|
|
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.
|
|
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
|
|
126
|
+
* Projects all items for public (unauthenticated) access
|
|
119
127
|
*/
|
|
120
|
-
|
|
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 `.
|
|
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 {
|
|
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
|
|
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 (!
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
302
|
-
|
|
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
|
+
}
|