@epsilon-asi/foundation 0.0.1-testing
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/.npm/_logs/2026-05-02T22_04_53_277Z-debug-0.log +22 -0
- package/.npm/_logs/2026-05-02T22_04_53_601Z-debug-0.log +18 -0
- package/.npm/_logs/2026-05-02T22_04_58_522Z-debug-0.log +18 -0
- package/.npm/_logs/2026-05-02T22_05_00_812Z-debug-0.log +19 -0
- package/.npm/_logs/2026-05-02T22_05_00_961Z-debug-0.log +14 -0
- package/build/decorators/factory/clone.d.ts +3 -0
- package/build/decorators/factory/clone.js +85 -0
- package/build/decorators/factory/decoratorRegistry.d.ts +2 -0
- package/build/decorators/factory/decoratorRegistry.js +213 -0
- package/build/decorators/factory/examples/accessors.d.ts +25 -0
- package/build/decorators/factory/examples/accessors.js +87 -0
- package/build/decorators/factory/examples/crud.d.ts +50 -0
- package/build/decorators/factory/examples/crud.js +120 -0
- package/build/decorators/factory/examples/orm.d.ts +45 -0
- package/build/decorators/factory/examples/orm.js +107 -0
- package/build/decorators/factory/examples/routing.d.ts +43 -0
- package/build/decorators/factory/examples/routing.js +112 -0
- package/build/decorators/factory/examples/runExamples.d.ts +1 -0
- package/build/decorators/factory/examples/runExamples.js +22 -0
- package/build/decorators/factory/examples/serialization.d.ts +24 -0
- package/build/decorators/factory/examples/serialization.js +79 -0
- package/build/decorators/factory/index.d.ts +3 -0
- package/build/decorators/factory/index.js +8 -0
- package/build/decorators/factory/snapshot.d.ts +2 -0
- package/build/decorators/factory/snapshot.js +75 -0
- package/build/decorators/factory/symbolMetadata.d.ts +2 -0
- package/build/decorators/factory/symbolMetadata.js +17 -0
- package/build/decorators/factory/types.d.ts +123 -0
- package/build/decorators/factory/types.js +2 -0
- package/build/decorators/validation/validatable.d.ts +0 -0
- package/build/decorators/validation/validatable.js +64 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +12 -0
- package/build/package.json +49 -0
- package/coverage/clover.xml +220 -0
- package/coverage/coverage-final.json +6 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/clone.ts.html +352 -0
- package/coverage/lcov-report/decoratorRegistry.ts.html +1183 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +176 -0
- package/coverage/lcov-report/index.ts.html +145 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/snapshot.ts.html +370 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/symbolMetadata.ts.html +145 -0
- package/coverage/lcov.info +490 -0
- package/package.json +47 -0
- package/src/decorators/factory/__tests__/cloneAndSnapshot.test.ts +223 -0
- package/src/decorators/factory/__tests__/decoratorRegistry.test.ts +292 -0
- package/src/decorators/factory/__tests__/decoratorRegistryEdges.test.ts +262 -0
- package/src/decorators/factory/clone.ts +89 -0
- package/src/decorators/factory/decoratorRegistry.ts +366 -0
- package/src/decorators/factory/examples/accessors.ts +38 -0
- package/src/decorators/factory/examples/crud.ts +82 -0
- package/src/decorators/factory/examples/orm.ts +83 -0
- package/src/decorators/factory/examples/routing.ts +70 -0
- package/src/decorators/factory/examples/runExamples.ts +22 -0
- package/src/decorators/factory/examples/serialization.ts +47 -0
- package/src/decorators/factory/index.ts +20 -0
- package/src/decorators/factory/snapshot.ts +95 -0
- package/src/decorators/factory/symbolMetadata.ts +20 -0
- package/src/decorators/factory/type-tests/decoratorRegistry.type-test.ts +302 -0
- package/src/decorators/factory/types.ts +259 -0
- package/src/decorators/validation/validatable.ts +63 -0
- package/src/index.ts +9 -0
- package/tsconfig.json +19 -0
- package/tsconfig.type-tests.json +8 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { cloneDeep, deepFreeze, immutableClone } from '../clone';
|
|
2
|
+
import { createSnapshot } from '../snapshot';
|
|
3
|
+
import type { DecorationRecord } from '../types';
|
|
4
|
+
|
|
5
|
+
describe('clone and freeze helpers', () => {
|
|
6
|
+
it('returns primitive values and functions without cloning them', () => {
|
|
7
|
+
const fn = () => 'ok';
|
|
8
|
+
|
|
9
|
+
expect(cloneDeep(undefined)).toBeUndefined();
|
|
10
|
+
expect(cloneDeep(null)).toBeNull();
|
|
11
|
+
expect(cloneDeep(123)).toBe(123);
|
|
12
|
+
expect(cloneDeep('value')).toBe('value');
|
|
13
|
+
expect(cloneDeep(fn)).toBe(fn);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('deep-clones dates, regexes, arrays, maps, sets, symbols, descriptors, prototypes, and circular references', () => {
|
|
17
|
+
const symbolKey = Symbol('secret');
|
|
18
|
+
const prototype = { inherited: true };
|
|
19
|
+
const source = Object.create(prototype) as {
|
|
20
|
+
date: Date;
|
|
21
|
+
regex: RegExp;
|
|
22
|
+
list: Array<{ value: string }>;
|
|
23
|
+
map: Map<{ key: string }, { value: string }>;
|
|
24
|
+
set: Set<{ value: string }>;
|
|
25
|
+
self?: unknown;
|
|
26
|
+
computed: string;
|
|
27
|
+
[symbolKey]: { nested: string };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const mapKey = { key: 'map-key' };
|
|
31
|
+
const mapValue = { value: 'map-value' };
|
|
32
|
+
const setValue = { value: 'set-value' };
|
|
33
|
+
|
|
34
|
+
source.date = new Date('2024-01-01T00:00:00.000Z');
|
|
35
|
+
source.regex = /users/gi;
|
|
36
|
+
source.list = [{ value: 'list-value' }];
|
|
37
|
+
source.map = new Map([[mapKey, mapValue]]);
|
|
38
|
+
source.set = new Set([setValue]);
|
|
39
|
+
source[symbolKey] = { nested: 'symbol-value' };
|
|
40
|
+
source.self = source;
|
|
41
|
+
Object.defineProperty(source, 'computed', {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
get() {
|
|
44
|
+
return 'computed-value';
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const cloned = cloneDeep(source);
|
|
49
|
+
|
|
50
|
+
expect(Object.getPrototypeOf(cloned)).toBe(prototype);
|
|
51
|
+
expect(cloned).not.toBe(source);
|
|
52
|
+
expect(cloned.date).not.toBe(source.date);
|
|
53
|
+
expect(cloned.date.getTime()).toBe(source.date.getTime());
|
|
54
|
+
expect(cloned.regex).not.toBe(source.regex);
|
|
55
|
+
expect(cloned.regex.source).toBe('users');
|
|
56
|
+
expect(cloned.regex.flags).toContain('g');
|
|
57
|
+
expect(cloned.regex.flags).toContain('i');
|
|
58
|
+
expect(cloned.list).not.toBe(source.list);
|
|
59
|
+
expect(cloned.list[0]).not.toBe(source.list[0]);
|
|
60
|
+
expect(cloned.list[0].value).toBe('list-value');
|
|
61
|
+
expect(cloned.self).toBe(cloned);
|
|
62
|
+
expect(cloned.computed).toBe('computed-value');
|
|
63
|
+
expect(cloned[symbolKey]).not.toBe(source[symbolKey]);
|
|
64
|
+
expect(cloned[symbolKey].nested).toBe('symbol-value');
|
|
65
|
+
|
|
66
|
+
const [[clonedMapKey, clonedMapValue]] = Array.from(cloned.map.entries());
|
|
67
|
+
expect(cloned.map).not.toBe(source.map);
|
|
68
|
+
expect(clonedMapKey).not.toBe(mapKey);
|
|
69
|
+
expect(clonedMapKey.key).toBe('map-key');
|
|
70
|
+
expect(clonedMapValue).not.toBe(mapValue);
|
|
71
|
+
expect(clonedMapValue.value).toBe('map-value');
|
|
72
|
+
|
|
73
|
+
const [clonedSetValue] = Array.from(cloned.set.values());
|
|
74
|
+
expect(cloned.set).not.toBe(source.set);
|
|
75
|
+
expect(clonedSetValue).not.toBe(setValue);
|
|
76
|
+
expect(clonedSetValue.value).toBe('set-value');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('deep-freezes objects, maps, sets, and cycles without infinite recursion', () => {
|
|
80
|
+
const objectValue: { nested: { value: string }; self?: unknown } = {
|
|
81
|
+
nested: { value: 'nested' },
|
|
82
|
+
};
|
|
83
|
+
objectValue.self = objectValue;
|
|
84
|
+
const mapValue = { value: 'map' };
|
|
85
|
+
const mapKey = { key: 'map-key' };
|
|
86
|
+
const setValue = { value: 'set' };
|
|
87
|
+
const container = {
|
|
88
|
+
objectValue,
|
|
89
|
+
map: new Map([[mapKey, mapValue]]),
|
|
90
|
+
set: new Set([setValue]),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const frozen = deepFreeze(container);
|
|
94
|
+
|
|
95
|
+
expect(frozen).toBe(container);
|
|
96
|
+
expect(Object.isFrozen(container)).toBe(true);
|
|
97
|
+
expect(Object.isFrozen(objectValue)).toBe(true);
|
|
98
|
+
expect(Object.isFrozen(objectValue.nested)).toBe(true);
|
|
99
|
+
expect(Object.isFrozen(container.map)).toBe(true);
|
|
100
|
+
expect(Object.isFrozen(mapKey)).toBe(true);
|
|
101
|
+
expect(Object.isFrozen(mapValue)).toBe(true);
|
|
102
|
+
expect(Object.isFrozen(container.set)).toBe(true);
|
|
103
|
+
expect(Object.isFrozen(setValue)).toBe(true);
|
|
104
|
+
expect(deepFreeze('already-primitive')).toBe('already-primitive');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('creates immutable clones that cannot be mutated through the original reference', () => {
|
|
108
|
+
const source = { nested: { value: 'before' }, list: [{ value: 'item' }] };
|
|
109
|
+
const cloned = immutableClone(source);
|
|
110
|
+
|
|
111
|
+
source.nested.value = 'after';
|
|
112
|
+
source.list[0].value = 'after';
|
|
113
|
+
|
|
114
|
+
expect(cloned.nested.value).toBe('before');
|
|
115
|
+
expect(cloned.list[0].value).toBe('item');
|
|
116
|
+
expect(Object.isFrozen(cloned)).toBe(true);
|
|
117
|
+
expect(Object.isFrozen(cloned.nested)).toBe(true);
|
|
118
|
+
expect(Object.isFrozen(cloned.list)).toBe(true);
|
|
119
|
+
expect(Object.isFrozen(cloned.list[0])).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('metadata snapshot helpers', () => {
|
|
124
|
+
const propertyWithoutName = Object.freeze({
|
|
125
|
+
id: 'snapshot:property:missing:0',
|
|
126
|
+
kind: 'property',
|
|
127
|
+
decorator: 'missingName',
|
|
128
|
+
metadata: undefined,
|
|
129
|
+
static: false,
|
|
130
|
+
private: false,
|
|
131
|
+
order: 0,
|
|
132
|
+
}) as DecorationRecord;
|
|
133
|
+
|
|
134
|
+
const records = Object.freeze([
|
|
135
|
+
Object.freeze({
|
|
136
|
+
id: 'snapshot:method:route:2',
|
|
137
|
+
kind: 'method',
|
|
138
|
+
decorator: 'route',
|
|
139
|
+
metadata: { path: '/later' },
|
|
140
|
+
memberName: 'later',
|
|
141
|
+
static: false,
|
|
142
|
+
private: false,
|
|
143
|
+
order: 2,
|
|
144
|
+
}) as DecorationRecord,
|
|
145
|
+
Object.freeze({
|
|
146
|
+
id: 'snapshot:class:entity:1',
|
|
147
|
+
kind: 'class',
|
|
148
|
+
decorator: 'entity',
|
|
149
|
+
metadata: { table: 'users' },
|
|
150
|
+
static: false,
|
|
151
|
+
private: false,
|
|
152
|
+
order: 1,
|
|
153
|
+
}) as DecorationRecord,
|
|
154
|
+
propertyWithoutName,
|
|
155
|
+
Object.freeze({
|
|
156
|
+
id: 'snapshot:property:column:3',
|
|
157
|
+
kind: 'property',
|
|
158
|
+
decorator: 'column',
|
|
159
|
+
metadata: { type: 'varchar' },
|
|
160
|
+
memberName: 'email',
|
|
161
|
+
static: false,
|
|
162
|
+
private: false,
|
|
163
|
+
order: 3,
|
|
164
|
+
}) as DecorationRecord,
|
|
165
|
+
Object.freeze({
|
|
166
|
+
id: 'snapshot:accessor:cached:4',
|
|
167
|
+
kind: 'accessor',
|
|
168
|
+
decorator: 'cached',
|
|
169
|
+
metadata: { ttlMs: 10 },
|
|
170
|
+
memberName: 'displayName',
|
|
171
|
+
static: false,
|
|
172
|
+
private: false,
|
|
173
|
+
order: 4,
|
|
174
|
+
}) as DecorationRecord,
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
it('sorts records, exposes filtered arrays, and returns frozen copies', () => {
|
|
178
|
+
const snapshot = createSnapshot(records);
|
|
179
|
+
|
|
180
|
+
expect(snapshot.all.map((record) => record.order)).toEqual([0, 1, 2, 3, 4]);
|
|
181
|
+
expect(Object.isFrozen(snapshot)).toBe(true);
|
|
182
|
+
expect(Object.isFrozen(snapshot.all)).toBe(true);
|
|
183
|
+
expect(Object.isFrozen(snapshot.classes)).toBe(true);
|
|
184
|
+
expect(Object.isFrozen(snapshot.properties)).toBe(true);
|
|
185
|
+
expect(Object.isFrozen(snapshot.methods)).toBe(true);
|
|
186
|
+
expect(Object.isFrozen(snapshot.accessors)).toBe(true);
|
|
187
|
+
|
|
188
|
+
expect(snapshot.firstClass('entity')?.metadata).toEqual({ table: 'users' });
|
|
189
|
+
expect(snapshot.firstClass('controller')).toBeUndefined();
|
|
190
|
+
expect(snapshot.classRecords()).toHaveLength(1);
|
|
191
|
+
expect(snapshot.classRecords('entity')).toHaveLength(1);
|
|
192
|
+
expect(snapshot.classRecords('controller')).toHaveLength(0);
|
|
193
|
+
expect(snapshot.propertyRecords()).toHaveLength(2);
|
|
194
|
+
expect(snapshot.propertyRecords('column')).toHaveLength(1);
|
|
195
|
+
expect(snapshot.methodRecords()).toHaveLength(1);
|
|
196
|
+
expect(snapshot.methodRecords('route')).toHaveLength(1);
|
|
197
|
+
expect(snapshot.methodRecords('missing')).toHaveLength(0);
|
|
198
|
+
expect(snapshot.accessorRecords()).toHaveLength(1);
|
|
199
|
+
expect(snapshot.accessorRecords('cached')).toHaveLength(1);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('builds readonly member maps with optional decorator filtering', () => {
|
|
203
|
+
const snapshot = createSnapshot(records);
|
|
204
|
+
|
|
205
|
+
const propertyMap = snapshot.propertyMap();
|
|
206
|
+
const columnMap = snapshot.propertyMap('column');
|
|
207
|
+
const methodMap = snapshot.methodMap();
|
|
208
|
+
const routeMap = snapshot.methodMap('route');
|
|
209
|
+
const missingMethodMap = snapshot.methodMap('missing');
|
|
210
|
+
const accessorMap = snapshot.accessorMap();
|
|
211
|
+
const cachedMap = snapshot.accessorMap('cached');
|
|
212
|
+
|
|
213
|
+
expect(propertyMap.has('email')).toBe(true);
|
|
214
|
+
expect(propertyMap.has('missingName')).toBe(false);
|
|
215
|
+
expect(columnMap.get('email')).toHaveLength(1);
|
|
216
|
+
expect(methodMap.get('later')).toHaveLength(1);
|
|
217
|
+
expect(routeMap.get('later')).toHaveLength(1);
|
|
218
|
+
expect(missingMethodMap.size).toBe(0);
|
|
219
|
+
expect(accessorMap.get('displayName')).toHaveLength(1);
|
|
220
|
+
expect(cachedMap.get('displayName')).toHaveLength(1);
|
|
221
|
+
expect(Object.isFrozen(columnMap.get('email'))).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { createDecoratorRegistry } from '../index';
|
|
2
|
+
|
|
3
|
+
type TestSchema = {
|
|
4
|
+
class: {
|
|
5
|
+
entity: { metadata: { table: string; nested?: { value: string } } };
|
|
6
|
+
controller: { metadata: { prefix: string } };
|
|
7
|
+
};
|
|
8
|
+
property: {
|
|
9
|
+
column: {
|
|
10
|
+
metadata: { type: 'uuid' | 'varchar'; primary?: boolean; nested?: { value: string } };
|
|
11
|
+
target: string;
|
|
12
|
+
};
|
|
13
|
+
serializable: {
|
|
14
|
+
metadata: undefined;
|
|
15
|
+
target: unknown;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
method: {
|
|
19
|
+
route: {
|
|
20
|
+
metadata: { method: 'GET' | 'POST'; path: string };
|
|
21
|
+
target: (...args: any[]) => Promise<unknown>;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
accessor: {
|
|
25
|
+
cached: {
|
|
26
|
+
metadata: { ttlMs?: number };
|
|
27
|
+
target: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type ControllerContract = {
|
|
33
|
+
id: string;
|
|
34
|
+
email: string;
|
|
35
|
+
get(id: string): Promise<{ id: string }>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function makeDecorators(namespace = 'test') {
|
|
39
|
+
const registry = createDecoratorRegistry<TestSchema>({ namespace });
|
|
40
|
+
const Scoped = registry.for<ControllerContract>();
|
|
41
|
+
return {
|
|
42
|
+
registry,
|
|
43
|
+
Entity: Scoped.class('entity'),
|
|
44
|
+
Controller: Scoped.class('controller'),
|
|
45
|
+
Column: Scoped.property('column'),
|
|
46
|
+
Serializable: Scoped.property('serializable'),
|
|
47
|
+
Route: Scoped.method('route'),
|
|
48
|
+
Cached: Scoped.accessor('cached'),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('modern decorator registry', () => {
|
|
53
|
+
it('creates an empty registry snapshot', () => {
|
|
54
|
+
const { registry } = makeDecorators('empty');
|
|
55
|
+
|
|
56
|
+
class Plain {}
|
|
57
|
+
|
|
58
|
+
const snapshot = registry.read(Plain);
|
|
59
|
+
expect(snapshot.all).toHaveLength(0);
|
|
60
|
+
expect(snapshot.classes).toHaveLength(0);
|
|
61
|
+
expect(snapshot.properties).toHaveLength(0);
|
|
62
|
+
expect(snapshot.methods).toHaveLength(0);
|
|
63
|
+
expect(snapshot.accessors).toHaveLength(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('registers class, property, method, and accessor metadata', () => {
|
|
67
|
+
const { registry, Entity, Column, Serializable, Route, Cached } = makeDecorators('all');
|
|
68
|
+
|
|
69
|
+
@Entity({ table: 'users' })
|
|
70
|
+
class UserController implements ControllerContract {
|
|
71
|
+
@Column('id', { type: 'uuid', primary: true })
|
|
72
|
+
id = 'user-1';
|
|
73
|
+
|
|
74
|
+
@Serializable.on('email')
|
|
75
|
+
@Column.on('email', { type: 'varchar' })
|
|
76
|
+
email = 'ray@example.com';
|
|
77
|
+
|
|
78
|
+
@Cached({ ttlMs: 50 })
|
|
79
|
+
accessor displayName = 'Ray';
|
|
80
|
+
|
|
81
|
+
@Route('get', { method: 'GET', path: '/users/:id' })
|
|
82
|
+
async get(id: string): Promise<{ id: string }> {
|
|
83
|
+
return { id };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const snapshot = registry.read(UserController);
|
|
88
|
+
|
|
89
|
+
expect(snapshot.firstClass('entity')?.metadata).toEqual({ table: 'users' });
|
|
90
|
+
expect(snapshot.propertyRecords('column')).toHaveLength(2);
|
|
91
|
+
expect(snapshot.propertyMap('column').get('id')?.[0].metadata).toEqual({
|
|
92
|
+
type: 'uuid',
|
|
93
|
+
primary: true,
|
|
94
|
+
});
|
|
95
|
+
expect(snapshot.propertyMap('serializable').get('email')).toHaveLength(1);
|
|
96
|
+
expect(snapshot.methodMap('route').get('get')?.[0].metadata).toEqual({
|
|
97
|
+
method: 'GET',
|
|
98
|
+
path: '/users/:id',
|
|
99
|
+
});
|
|
100
|
+
expect(snapshot.accessorMap('cached').get('displayName')?.[0].metadata).toEqual({
|
|
101
|
+
ttlMs: 50,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('reads metadata from an instance', () => {
|
|
106
|
+
const { registry, Entity, Column, Route } = makeDecorators('instance');
|
|
107
|
+
|
|
108
|
+
@Entity({ table: 'users' })
|
|
109
|
+
class UserController implements ControllerContract {
|
|
110
|
+
@Column({ type: 'uuid' })
|
|
111
|
+
id = 'user-1';
|
|
112
|
+
|
|
113
|
+
email = 'ray@example.com';
|
|
114
|
+
|
|
115
|
+
@Route({ method: 'GET', path: '/users/:id' })
|
|
116
|
+
async get(id: string): Promise<{ id: string }> {
|
|
117
|
+
return { id };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const snapshot = registry.read(new UserController());
|
|
122
|
+
expect(snapshot.firstClass('entity')?.metadata).toEqual({ table: 'users' });
|
|
123
|
+
expect(snapshot.propertyMap('column').get('id')).toHaveLength(1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('deep-clones and freezes metadata at registration time', () => {
|
|
127
|
+
const { registry, Entity, Column } = makeDecorators('immutability');
|
|
128
|
+
const classMetadata = { table: 'users', nested: { value: 'before' } };
|
|
129
|
+
const columnMetadata = { type: 'varchar' as const, nested: { value: 'before' } };
|
|
130
|
+
|
|
131
|
+
@Entity(classMetadata)
|
|
132
|
+
class UserController implements ControllerContract {
|
|
133
|
+
id = 'user-1';
|
|
134
|
+
|
|
135
|
+
@Column(columnMetadata)
|
|
136
|
+
email = 'ray@example.com';
|
|
137
|
+
|
|
138
|
+
async get(id: string): Promise<{ id: string }> {
|
|
139
|
+
return { id };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
classMetadata.nested.value = 'after';
|
|
144
|
+
columnMetadata.nested.value = 'after';
|
|
145
|
+
|
|
146
|
+
const snapshot = registry.read(UserController);
|
|
147
|
+
const entityMetadata = snapshot.firstClass('entity')?.metadata as typeof classMetadata;
|
|
148
|
+
const columnRecord = snapshot.propertyMap('column').get('email')?.[0];
|
|
149
|
+
const storedColumnMetadata = columnRecord?.metadata as typeof columnMetadata;
|
|
150
|
+
|
|
151
|
+
expect(entityMetadata.nested.value).toBe('before');
|
|
152
|
+
expect(storedColumnMetadata.nested.value).toBe('before');
|
|
153
|
+
expect(Object.isFrozen(entityMetadata)).toBe(true);
|
|
154
|
+
expect(Object.isFrozen(entityMetadata.nested)).toBe(true);
|
|
155
|
+
expect(Object.isFrozen(storedColumnMetadata)).toBe(true);
|
|
156
|
+
expect(Object.isFrozen(storedColumnMetadata.nested)).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('includes inherited metadata by default and can read own metadata only', () => {
|
|
160
|
+
const { registry, Column, Serializable } = makeDecorators('inheritance');
|
|
161
|
+
|
|
162
|
+
class BaseController {
|
|
163
|
+
@Serializable()
|
|
164
|
+
id = 'base-id';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
class UserController extends BaseController implements ControllerContract {
|
|
168
|
+
@Column({ type: 'varchar' })
|
|
169
|
+
email = 'ray@example.com';
|
|
170
|
+
|
|
171
|
+
async get(id: string): Promise<{ id: string }> {
|
|
172
|
+
return { id };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const inherited = registry.read(UserController);
|
|
177
|
+
expect(inherited.propertyRecords('serializable')).toHaveLength(1);
|
|
178
|
+
expect(inherited.propertyRecords('column')).toHaveLength(1);
|
|
179
|
+
|
|
180
|
+
const ownOnly = registry.read(UserController, { includeInherited: false });
|
|
181
|
+
expect(ownOnly.propertyRecords('serializable')).toHaveLength(0);
|
|
182
|
+
expect(ownOnly.propertyRecords('column')).toHaveLength(1);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('preserves symbol keys', () => {
|
|
186
|
+
const { registry, Column } = makeDecorators('symbol');
|
|
187
|
+
const secret = Symbol('secret');
|
|
188
|
+
|
|
189
|
+
class SymbolExample {
|
|
190
|
+
@Column({ type: 'varchar' })
|
|
191
|
+
[secret] = 'hidden';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const snapshot = registry.read(SymbolExample);
|
|
195
|
+
expect(snapshot.propertyRecords('column')[0].memberName).toBe(secret);
|
|
196
|
+
expect(snapshot.propertyMap('column').get(secret)).toHaveLength(1);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('rejects private members by default', () => {
|
|
200
|
+
const { Column } = makeDecorators('private-reject');
|
|
201
|
+
|
|
202
|
+
expect(() => {
|
|
203
|
+
class PrivateExample {
|
|
204
|
+
@Column({ type: 'varchar' })
|
|
205
|
+
#secret = 'hidden';
|
|
206
|
+
|
|
207
|
+
reveal() {
|
|
208
|
+
return this.#secret;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return PrivateExample;
|
|
213
|
+
}).toThrow(/allowPrivateMembers/);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('allows private members when configured', () => {
|
|
217
|
+
const registry = createDecoratorRegistry<TestSchema>({
|
|
218
|
+
namespace: 'private-allow',
|
|
219
|
+
allowPrivateMembers: true,
|
|
220
|
+
});
|
|
221
|
+
const Column = registry.for<{ secret: string }>().property('column');
|
|
222
|
+
|
|
223
|
+
class PrivateExample {
|
|
224
|
+
@Column({ type: 'varchar' })
|
|
225
|
+
#secret = 'hidden';
|
|
226
|
+
|
|
227
|
+
reveal() {
|
|
228
|
+
return this.#secret;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const snapshot = registry.read(PrivateExample);
|
|
233
|
+
expect(snapshot.propertyRecords('column')).toHaveLength(1);
|
|
234
|
+
expect(snapshot.propertyRecords('column')[0].private).toBe(true);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('marks static member records', () => {
|
|
238
|
+
const { registry, Column, Route } = makeDecorators('static');
|
|
239
|
+
|
|
240
|
+
class StaticExample {
|
|
241
|
+
@Column({ type: 'varchar' })
|
|
242
|
+
static tableName = 'users';
|
|
243
|
+
|
|
244
|
+
@Route({ method: 'GET', path: '/health' })
|
|
245
|
+
static async health(): Promise<{ id: string }> {
|
|
246
|
+
return { id: 'ok' };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
expect(registry.read(StaticExample).propertyRecords('column')[0].static).toBe(true);
|
|
251
|
+
expect(registry.read(StaticExample).methodRecords('route')[0].static).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('isolates records by registry namespace and store', () => {
|
|
255
|
+
type IsolatedSchema = {
|
|
256
|
+
class: { marker: { metadata: { value: string } } };
|
|
257
|
+
property: {};
|
|
258
|
+
method: {};
|
|
259
|
+
accessor: {};
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const first = createDecoratorRegistry<IsolatedSchema>({ namespace: 'first' });
|
|
263
|
+
const second = createDecoratorRegistry<IsolatedSchema>({ namespace: 'second' });
|
|
264
|
+
const FirstMarker = first.for<object>().class('marker');
|
|
265
|
+
const SecondMarker = second.for<object>().class('marker');
|
|
266
|
+
|
|
267
|
+
@FirstMarker({ value: 'one' })
|
|
268
|
+
@SecondMarker({ value: 'two' })
|
|
269
|
+
class Marked {}
|
|
270
|
+
|
|
271
|
+
expect(first.read(Marked).classes).toHaveLength(1);
|
|
272
|
+
expect(second.read(Marked).classes).toHaveLength(1);
|
|
273
|
+
expect(first.read(Marked).classes[0].id).toContain('first:class:marker');
|
|
274
|
+
expect(second.read(Marked).classes[0].id).toContain('second:class:marker');
|
|
275
|
+
expect(first.read(Marked).classes[0].metadata).toEqual({ value: 'one' });
|
|
276
|
+
expect(second.read(Marked).classes[0].metadata).toEqual({ value: 'two' });
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('throws at runtime when a forced keyed decorator is applied to the wrong member', () => {
|
|
280
|
+
const { Column } = makeDecorators('forced-key');
|
|
281
|
+
const ForcedColumn = Column as unknown as (...args: unknown[]) => Function;
|
|
282
|
+
|
|
283
|
+
expect(() => {
|
|
284
|
+
class ForcedBadKey {
|
|
285
|
+
@ForcedColumn('email', { type: 'varchar' })
|
|
286
|
+
id = 'user-1';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return ForcedBadKey;
|
|
290
|
+
}).toThrow(/keyed for/);
|
|
291
|
+
});
|
|
292
|
+
});
|