@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,262 @@
|
|
|
1
|
+
import { createDecoratorRegistry } from '../index';
|
|
2
|
+
import { ensureSymbolMetadata, symbolMetadata } from '../symbolMetadata';
|
|
3
|
+
|
|
4
|
+
type EdgeSchema = {
|
|
5
|
+
class: {
|
|
6
|
+
entity: { metadata: { table: string } };
|
|
7
|
+
tag: { metadata: undefined };
|
|
8
|
+
};
|
|
9
|
+
property: {
|
|
10
|
+
column: {
|
|
11
|
+
metadata: { type: 'uuid' | 'varchar'; primary?: boolean };
|
|
12
|
+
target: string;
|
|
13
|
+
};
|
|
14
|
+
serializable: {
|
|
15
|
+
metadata: undefined;
|
|
16
|
+
target: unknown;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
method: {
|
|
20
|
+
route: {
|
|
21
|
+
metadata: { method: 'GET' | 'POST'; path: string };
|
|
22
|
+
target: (...args: any[]) => Promise<unknown>;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
accessor: {
|
|
26
|
+
cached: {
|
|
27
|
+
metadata: { ttlMs?: number };
|
|
28
|
+
target: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type EdgeContract = {
|
|
34
|
+
id: string;
|
|
35
|
+
email: string;
|
|
36
|
+
displayName: string;
|
|
37
|
+
get(id: string): Promise<{ id: string }>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function fakeMetadataContext(
|
|
41
|
+
kind: 'class' | 'field' | 'method' | 'accessor' | 'getter' | 'setter',
|
|
42
|
+
overrides: Partial<
|
|
43
|
+
ClassDecoratorContext | ClassFieldDecoratorContext | ClassMethodDecoratorContext | ClassAccessorDecoratorContext
|
|
44
|
+
> = {},
|
|
45
|
+
) {
|
|
46
|
+
return {
|
|
47
|
+
kind,
|
|
48
|
+
name: kind === 'class' ? 'FakeClass' : 'member',
|
|
49
|
+
static: false,
|
|
50
|
+
private: false,
|
|
51
|
+
metadata: {},
|
|
52
|
+
addInitializer: jest.fn(),
|
|
53
|
+
access: {},
|
|
54
|
+
...overrides,
|
|
55
|
+
} as any;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('modern decorator registry edge coverage', () => {
|
|
59
|
+
it('uses default options and returns empty snapshots for objects without constructors', () => {
|
|
60
|
+
const registry = createDecoratorRegistry<EdgeSchema>();
|
|
61
|
+
const targetWithoutConstructor = Object.create(null);
|
|
62
|
+
|
|
63
|
+
expect(registry.namespace).toBe('default');
|
|
64
|
+
expect(registry.allowPrivateMembers).toBe(false);
|
|
65
|
+
expect(registry.read(targetWithoutConstructor).all).toHaveLength(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('supports unscoped class decorators and no-metadata class decorators', () => {
|
|
69
|
+
const registry = createDecoratorRegistry<EdgeSchema>({ namespace: 'unscoped' });
|
|
70
|
+
const Entity = registry.class('entity');
|
|
71
|
+
const Tag = registry.class('tag') as unknown as () => ClassDecorator;
|
|
72
|
+
|
|
73
|
+
@Entity<EdgeContract>({ table: 'users' })
|
|
74
|
+
@Tag()
|
|
75
|
+
class UserController implements EdgeContract {
|
|
76
|
+
id = 'user-1';
|
|
77
|
+
email = 'ray@example.com';
|
|
78
|
+
displayName = 'Ray';
|
|
79
|
+
|
|
80
|
+
async get(id: string): Promise<{ id: string }> {
|
|
81
|
+
return { id };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const snapshot = registry.read(UserController);
|
|
86
|
+
expect(snapshot.classRecords()).toHaveLength(2);
|
|
87
|
+
expect(snapshot.firstClass('entity')?.metadata).toEqual({ table: 'users' });
|
|
88
|
+
expect(snapshot.firstClass('tag')?.metadata).toBeUndefined();
|
|
89
|
+
expect(snapshot.classes.map((record) => record.id)).toEqual([
|
|
90
|
+
'unscoped:class:tag:0',
|
|
91
|
+
'unscoped:class:entity:1',
|
|
92
|
+
]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('supports direct and .on keyed forms for property, method, and accessor decorators', () => {
|
|
96
|
+
const registry = createDecoratorRegistry<EdgeSchema>({ namespace: 'keyed-forms' });
|
|
97
|
+
const scoped = registry.for<EdgeContract>();
|
|
98
|
+
const Column = scoped.property('column');
|
|
99
|
+
const Serializable = scoped.property('serializable');
|
|
100
|
+
const Route = scoped.method('route');
|
|
101
|
+
|
|
102
|
+
class UserController implements EdgeContract {
|
|
103
|
+
@Column('id', { type: 'uuid', primary: true })
|
|
104
|
+
id = 'user-1';
|
|
105
|
+
|
|
106
|
+
@Serializable.on('email')
|
|
107
|
+
@Column.on('email', { type: 'varchar' })
|
|
108
|
+
email = 'ray@example.com';
|
|
109
|
+
|
|
110
|
+
displayName = 'Ray';
|
|
111
|
+
|
|
112
|
+
@Route.on('get', { method: 'GET', path: '/users/:id' })
|
|
113
|
+
async get(id: string): Promise<{ id: string }> {
|
|
114
|
+
return { id };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const snapshot = registry.read(UserController);
|
|
119
|
+
expect(snapshot.propertyMap('column').get('id')?.[0].metadata).toEqual({
|
|
120
|
+
type: 'uuid',
|
|
121
|
+
primary: true,
|
|
122
|
+
});
|
|
123
|
+
expect(snapshot.propertyMap('serializable').get('email')?.[0].metadata).toBeUndefined();
|
|
124
|
+
expect(snapshot.methodMap('route').get('get')?.[0].metadata).toEqual({
|
|
125
|
+
method: 'GET',
|
|
126
|
+
path: '/users/:id',
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('records getter and setter decorators as accessor metadata', () => {
|
|
131
|
+
const registry = createDecoratorRegistry<EdgeSchema>({ namespace: 'getter-setter' });
|
|
132
|
+
const Cached = registry.for<EdgeContract>().accessor('cached');
|
|
133
|
+
|
|
134
|
+
class UserView {
|
|
135
|
+
private name = 'Ray';
|
|
136
|
+
|
|
137
|
+
@Cached({ ttlMs: 10 })
|
|
138
|
+
get displayName(): string {
|
|
139
|
+
return this.name;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@Cached({ ttlMs: 20 })
|
|
143
|
+
set displayName(value: string) {
|
|
144
|
+
this.name = value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const records = registry.read(UserView).accessorMap('cached').get('displayName');
|
|
149
|
+
expect(records).toHaveLength(2);
|
|
150
|
+
expect(records?.map((record) => record.metadata)).toEqual([{ ttlMs: 10 }, { ttlMs: 20 }]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('throws for invalid direct keys and invalid .on arguments', () => {
|
|
154
|
+
const registry = createDecoratorRegistry<EdgeSchema>({ namespace: 'bad-keys' });
|
|
155
|
+
const Column = registry.for<EdgeContract>().property('column') as unknown as {
|
|
156
|
+
(...args: unknown[]): unknown;
|
|
157
|
+
on(...args: unknown[]): unknown;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
expect(() => Column(123, { type: 'varchar' })).toThrow(/expected a string or symbol/);
|
|
161
|
+
expect(() => Column.on()).toThrow(/requires a member key/);
|
|
162
|
+
expect(() => Column.on({ not: 'a-key' }, { type: 'varchar' })).toThrow(
|
|
163
|
+
/expected a string or symbol/,
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('throws when decorators are manually applied to the wrong context kind', () => {
|
|
168
|
+
const registry = createDecoratorRegistry<EdgeSchema>({ namespace: 'wrong-kind' });
|
|
169
|
+
const scoped = registry.for<EdgeContract>();
|
|
170
|
+
const Entity = scoped.class('entity');
|
|
171
|
+
const Column = scoped.property('column');
|
|
172
|
+
const Route = scoped.method('route');
|
|
173
|
+
const Cached = scoped.accessor('cached');
|
|
174
|
+
|
|
175
|
+
expect(() =>
|
|
176
|
+
(Entity({ table: 'users' }) as any)(class Fake {}, fakeMetadataContext('method')),
|
|
177
|
+
).toThrow(/can only be used on class targets/);
|
|
178
|
+
|
|
179
|
+
expect(() =>
|
|
180
|
+
(Column({ type: 'varchar' }) as any)(undefined, fakeMetadataContext('method')),
|
|
181
|
+
).toThrow(/can only be used on property targets/);
|
|
182
|
+
|
|
183
|
+
expect(() =>
|
|
184
|
+
(Route({ method: 'GET', path: '/x' }) as any)(async () => undefined, fakeMetadataContext('field')),
|
|
185
|
+
).toThrow(/can only be used on method targets/);
|
|
186
|
+
|
|
187
|
+
expect(() =>
|
|
188
|
+
(Cached({ ttlMs: 1 }) as any)(undefined, fakeMetadataContext('field')),
|
|
189
|
+
).toThrow(/can only be used on accessor targets/);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('throws when a keyed decorator is applied to a different member at runtime', () => {
|
|
193
|
+
const registry = createDecoratorRegistry<EdgeSchema>({ namespace: 'runtime-key-mismatch' });
|
|
194
|
+
const Column = registry.for<EdgeContract>().property('column') as unknown as (...args: unknown[]) => any;
|
|
195
|
+
const decorator = Column('email', { type: 'varchar' });
|
|
196
|
+
|
|
197
|
+
expect(() =>
|
|
198
|
+
decorator(undefined, fakeMetadataContext('field', { name: 'id' })),
|
|
199
|
+
).toThrow(/keyed for/);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('allows manually decorated private members when configured', () => {
|
|
203
|
+
const registry = createDecoratorRegistry<EdgeSchema>({
|
|
204
|
+
namespace: 'manual-private',
|
|
205
|
+
allowPrivateMembers: true,
|
|
206
|
+
});
|
|
207
|
+
const Column = registry.for<EdgeContract>().property('column');
|
|
208
|
+
const metadataObject = {};
|
|
209
|
+
const decorator = Column({ type: 'varchar' });
|
|
210
|
+
|
|
211
|
+
(decorator as any)(undefined, fakeMetadataContext('field', {
|
|
212
|
+
name: '#secret',
|
|
213
|
+
private: true,
|
|
214
|
+
metadata: metadataObject,
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
const Manual = class {};
|
|
218
|
+
Object.defineProperty(Manual, symbolMetadata, {
|
|
219
|
+
configurable: true,
|
|
220
|
+
value: metadataObject,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const record = registry.read(Manual).propertyRecords('column')[0];
|
|
224
|
+
expect(record.private).toBe(true);
|
|
225
|
+
expect(record.memberName).toBe('#secret');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('accepts manually applied getter and setter contexts for accessor decorators', () => {
|
|
229
|
+
const registry = createDecoratorRegistry<EdgeSchema>({ namespace: 'manual-accessor-kinds' });
|
|
230
|
+
const Cached = registry.for<EdgeContract>().accessor('cached');
|
|
231
|
+
const metadataObject = {};
|
|
232
|
+
|
|
233
|
+
(Cached({ ttlMs: 1 }) as any)(() => 'Ray', fakeMetadataContext('getter', {
|
|
234
|
+
name: 'displayName',
|
|
235
|
+
metadata: metadataObject,
|
|
236
|
+
}));
|
|
237
|
+
(Cached({ ttlMs: 2 }) as any)((_value: string) => undefined, fakeMetadataContext('setter', {
|
|
238
|
+
name: 'displayName',
|
|
239
|
+
metadata: metadataObject,
|
|
240
|
+
}));
|
|
241
|
+
|
|
242
|
+
const Manual = class {};
|
|
243
|
+
Object.defineProperty(Manual, symbolMetadata, {
|
|
244
|
+
configurable: true,
|
|
245
|
+
value: metadataObject,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(registry.read(Manual).accessorRecords('cached').map((record) => record.metadata)).toEqual([
|
|
249
|
+
{ ttlMs: 1 },
|
|
250
|
+
{ ttlMs: 2 },
|
|
251
|
+
]);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('reuses Symbol.metadata once the polyfill has been installed', () => {
|
|
255
|
+
const first = ensureSymbolMetadata();
|
|
256
|
+
const second = ensureSymbolMetadata();
|
|
257
|
+
|
|
258
|
+
expect(first).toBe(second);
|
|
259
|
+
expect(first).toBe(symbolMetadata);
|
|
260
|
+
expect((Symbol as SymbolConstructor & { metadata?: symbol }).metadata).toBe(symbolMetadata);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
function isObjectLike(value: unknown): value is object {
|
|
2
|
+
return (typeof value === 'object' && value !== null) || typeof value === 'function';
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function cloneDeep<T>(value: T, seen = new WeakMap<object, unknown>()): T {
|
|
6
|
+
if (!isObjectLike(value)) return value;
|
|
7
|
+
|
|
8
|
+
if (typeof value === 'function') return value;
|
|
9
|
+
|
|
10
|
+
const existing = seen.get(value);
|
|
11
|
+
if (existing !== undefined) return existing as T;
|
|
12
|
+
|
|
13
|
+
if (value instanceof Date) {
|
|
14
|
+
return new Date(value.getTime()) as T;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (value instanceof RegExp) {
|
|
18
|
+
return new RegExp(value.source, value.flags) as T;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (value instanceof Map) {
|
|
22
|
+
const cloned = new Map<unknown, unknown>();
|
|
23
|
+
seen.set(value, cloned);
|
|
24
|
+
for (const [key, mapValue] of value.entries()) {
|
|
25
|
+
cloned.set(cloneDeep(key, seen), cloneDeep(mapValue, seen));
|
|
26
|
+
}
|
|
27
|
+
return cloned as T;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (value instanceof Set) {
|
|
31
|
+
const cloned = new Set<unknown>();
|
|
32
|
+
seen.set(value, cloned);
|
|
33
|
+
for (const setValue of value.values()) {
|
|
34
|
+
cloned.add(cloneDeep(setValue, seen));
|
|
35
|
+
}
|
|
36
|
+
return cloned as T;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
const cloned: unknown[] = [];
|
|
41
|
+
seen.set(value, cloned);
|
|
42
|
+
for (const item of value) cloned.push(cloneDeep(item, seen));
|
|
43
|
+
return cloned as T;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const cloned = Object.create(Object.getPrototypeOf(value)) as Record<PropertyKey, unknown>;
|
|
47
|
+
seen.set(value, cloned);
|
|
48
|
+
|
|
49
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
50
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
51
|
+
if (descriptor === undefined) continue;
|
|
52
|
+
|
|
53
|
+
if ('value' in descriptor) {
|
|
54
|
+
descriptor.value = cloneDeep(descriptor.value, seen);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
Object.defineProperty(cloned, key, descriptor);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return cloned as T;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function deepFreeze<T>(value: T, seen = new WeakSet<object>()): T {
|
|
64
|
+
if (!isObjectLike(value)) return value;
|
|
65
|
+
if (seen.has(value)) return value;
|
|
66
|
+
|
|
67
|
+
seen.add(value);
|
|
68
|
+
|
|
69
|
+
if (value instanceof Map) {
|
|
70
|
+
for (const [key, mapValue] of value.entries()) {
|
|
71
|
+
deepFreeze(key, seen);
|
|
72
|
+
deepFreeze(mapValue, seen);
|
|
73
|
+
}
|
|
74
|
+
} else if (value instanceof Set) {
|
|
75
|
+
for (const setValue of value.values()) {
|
|
76
|
+
deepFreeze(setValue, seen);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
80
|
+
deepFreeze((value as Record<PropertyKey, unknown>)[key], seen);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return Object.freeze(value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function immutableClone<T>(value: T): T {
|
|
88
|
+
return deepFreeze(cloneDeep(value));
|
|
89
|
+
}
|