@cratis/fundamentals 7.17.0 → 7.18.0

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.
Files changed (95) hide show
  1. package/Fields.ts +9 -3
  2. package/StandardClassDecoratorContext.ts +14 -0
  3. package/StandardFieldDecoratorContext.ts +13 -0
  4. package/derivedTypeDecorator.ts +21 -4
  5. package/dist/Fields.d.ts.map +1 -1
  6. package/dist/Fields.js +4 -0
  7. package/dist/Fields.js.map +1 -1
  8. package/dist/StandardClassDecoratorContext.d.ts +8 -0
  9. package/dist/StandardClassDecoratorContext.d.ts.map +1 -0
  10. package/dist/StandardClassDecoratorContext.js +2 -0
  11. package/dist/StandardClassDecoratorContext.js.map +1 -0
  12. package/dist/StandardFieldDecoratorContext.d.ts +8 -0
  13. package/dist/StandardFieldDecoratorContext.d.ts.map +1 -0
  14. package/dist/StandardFieldDecoratorContext.js +2 -0
  15. package/dist/StandardFieldDecoratorContext.js.map +1 -0
  16. package/dist/cjs/Fields.d.ts.map +1 -1
  17. package/dist/cjs/Fields.js +4 -0
  18. package/dist/cjs/Fields.js.map +1 -1
  19. package/dist/cjs/StandardClassDecoratorContext.d.ts +8 -0
  20. package/dist/cjs/StandardClassDecoratorContext.d.ts.map +1 -0
  21. package/dist/cjs/StandardFieldDecoratorContext.d.ts +8 -0
  22. package/dist/cjs/StandardFieldDecoratorContext.d.ts.map +1 -0
  23. package/dist/cjs/derivedTypeDecorator.d.ts +5 -1
  24. package/dist/cjs/derivedTypeDecorator.d.ts.map +1 -1
  25. package/dist/cjs/derivedTypeDecorator.js +18 -2
  26. package/dist/cjs/derivedTypeDecorator.js.map +1 -1
  27. package/dist/cjs/fieldDecorator.d.ts +5 -1
  28. package/dist/cjs/fieldDecorator.d.ts.map +1 -1
  29. package/dist/cjs/fieldDecorator.js +33 -2
  30. package/dist/cjs/fieldDecorator.js.map +1 -1
  31. package/dist/cjs/fieldDecoratorMetadata.d.ts +6 -0
  32. package/dist/cjs/fieldDecoratorMetadata.d.ts.map +1 -0
  33. package/dist/cjs/fieldDecoratorMetadata.js +23 -0
  34. package/dist/cjs/fieldDecoratorMetadata.js.map +1 -0
  35. package/dist/cjs/reflection.d.ts.map +1 -1
  36. package/dist/cjs/reflection.js +9 -0
  37. package/dist/cjs/reflection.js.map +1 -1
  38. package/dist/derivedTypeDecorator.d.ts +5 -1
  39. package/dist/derivedTypeDecorator.d.ts.map +1 -1
  40. package/dist/derivedTypeDecorator.js +18 -2
  41. package/dist/derivedTypeDecorator.js.map +1 -1
  42. package/dist/esm/Fields.d.ts.map +1 -1
  43. package/dist/esm/Fields.js +4 -0
  44. package/dist/esm/Fields.js.map +1 -1
  45. package/dist/esm/StandardClassDecoratorContext.d.ts +8 -0
  46. package/dist/esm/StandardClassDecoratorContext.d.ts.map +1 -0
  47. package/dist/esm/StandardFieldDecoratorContext.d.ts +8 -0
  48. package/dist/esm/StandardFieldDecoratorContext.d.ts.map +1 -0
  49. package/dist/esm/derivedTypeDecorator.d.ts +5 -1
  50. package/dist/esm/derivedTypeDecorator.d.ts.map +1 -1
  51. package/dist/esm/derivedTypeDecorator.js +18 -2
  52. package/dist/esm/derivedTypeDecorator.js.map +1 -1
  53. package/dist/esm/fieldDecorator.d.ts +5 -1
  54. package/dist/esm/fieldDecorator.d.ts.map +1 -1
  55. package/dist/esm/fieldDecorator.js +33 -2
  56. package/dist/esm/fieldDecorator.js.map +1 -1
  57. package/dist/esm/fieldDecoratorMetadata.d.ts +6 -0
  58. package/dist/esm/fieldDecoratorMetadata.d.ts.map +1 -0
  59. package/dist/esm/fieldDecoratorMetadata.js +20 -0
  60. package/dist/esm/fieldDecoratorMetadata.js.map +1 -0
  61. package/dist/esm/reflection.d.ts.map +1 -1
  62. package/dist/esm/reflection.js +9 -0
  63. package/dist/esm/reflection.js.map +1 -1
  64. package/dist/fieldDecorator.d.ts +5 -1
  65. package/dist/fieldDecorator.d.ts.map +1 -1
  66. package/dist/fieldDecorator.js +33 -2
  67. package/dist/fieldDecorator.js.map +1 -1
  68. package/dist/fieldDecoratorMetadata.d.ts +6 -0
  69. package/dist/fieldDecoratorMetadata.d.ts.map +1 -0
  70. package/dist/fieldDecoratorMetadata.js +17 -0
  71. package/dist/fieldDecoratorMetadata.js.map +1 -0
  72. package/dist/reflection.d.ts.map +1 -1
  73. package/dist/reflection.js +9 -0
  74. package/dist/reflection.js.map +1 -1
  75. package/dist/tsconfig.tsbuildinfo +1 -1
  76. package/fieldDecorator.ts +36 -5
  77. package/fieldDecoratorMetadata.ts +34 -0
  78. package/for_JsonSerializer/when_metadata_is_written_by_another_copy_of_the_package.ts +23 -5
  79. package/for_derivedTypeDecorator/when_an_outer_standard_decorator_replaces_the_class.ts +28 -0
  80. package/for_fieldDecorator/fixtures/standard_decorated_types.ts +76 -0
  81. package/for_fieldDecorator/when_executing_standard_typescript_decorator_emit.ts +109 -0
  82. package/for_fieldDecorator/when_invoked_as_a_legacy_decorator.ts +21 -0
  83. package/for_fieldDecorator/when_invoked_as_a_standard_decorator.ts +43 -0
  84. package/for_fieldDecorator/when_invoked_for_an_unsupported_legacy_field/with_a_static_field.ts +12 -0
  85. package/for_fieldDecorator/when_invoked_for_an_unsupported_legacy_field/with_a_symbol_name.ts +12 -0
  86. package/for_fieldDecorator/when_invoked_for_an_unsupported_standard_field/with_a_private_field.ts +16 -0
  87. package/for_fieldDecorator/when_invoked_for_an_unsupported_standard_field/with_a_static_field.ts +16 -0
  88. package/for_fieldDecorator/when_invoked_for_an_unsupported_standard_field/with_a_symbol_name.ts +16 -0
  89. package/for_fieldDecorator/when_invoked_for_an_unsupported_standard_field/without_decorator_metadata.ts +16 -0
  90. package/for_fieldDecorator/when_legacy_and_standard_metadata_are_mixed_across_inheritance.ts +43 -0
  91. package/for_fieldDecorator/when_standard_metadata_contains_unrelated_fields_metadata.ts +34 -0
  92. package/for_fieldDecorator/when_standard_metadata_is_inherited_and_overridden.ts +37 -0
  93. package/for_reflection/when_loading_with_existing_symbol_metadata.ts +25 -0
  94. package/package.json +1 -1
  95. package/reflection.ts +13 -0
@@ -2,17 +2,17 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import { DerivedType } from '../DerivedType';
5
+ import { Field } from '../Field';
5
6
  import { Fields } from '../Fields';
7
+ import { addFieldToDecoratorMetadata, getOwnDecoratorFieldsForType } from '../fieldDecoratorMetadata';
6
8
 
7
9
  /**
8
10
  * Pins why `Fields` and `DerivedType` were left keyed on the constructor while the converter registry
9
11
  * was not.
10
12
  *
11
- * Neither holds state in module scope. Both write reflect metadata keyed on the target constructor, so
12
- * the state lives with the type being described rather than with the class describing it, and a
13
- * duplicated `Fields` reads what another `Fields` wrote. (In a real duplicate install the two copies
14
- * also share one metadata store, because the polyfill installs only when the global `Reflect` lacks it
15
- * - here a single `reflection` module is loaded, which is the same store by construction.)
13
+ * Neither holds state in module scope. Legacy metadata is keyed on the target constructor and standard
14
+ * metadata is keyed on the metadata object attached to that constructor. Both live in the shared
15
+ * Reflect metadata store, so a duplicated reader can read what another package copy wrote.
16
16
  *
17
17
  * That is the whole reason those two kept comparing constructors while the converter registry stopped.
18
18
  * If it ever stops being true, this goes red and they need the same treatment the converters got.
@@ -22,25 +22,43 @@ describe('when metadata is written by another copy of the package', () => {
22
22
  let derivedTypeIsDistinct: boolean;
23
23
  let fieldNamesReadByTheOtherCopy: string[];
24
24
  let derivedTypeReadByTheOtherCopy: string | undefined;
25
+ let standardFieldNamesReadByOtherFieldsCopy: string[];
26
+ let standardFieldNamesReadByTheOtherCopy: string[];
27
+ let standardMetadataCopiesAreDistinct: boolean;
25
28
 
26
29
  beforeEach(async () => {
27
30
  const otherFields = await import(/* @vite-ignore */ '../Fields?anotherCopy') as typeof import('../Fields');
28
31
  const otherDerivedType = await import(/* @vite-ignore */ '../DerivedType?anotherCopy') as typeof import('../DerivedType');
32
+ const standardMetadataWriter = await import(/* @vite-ignore */ '../fieldDecoratorMetadata?writerCopy') as typeof import('../fieldDecoratorMetadata');
33
+ const standardMetadataReader = await import(/* @vite-ignore */ '../fieldDecoratorMetadata?readerCopy') as typeof import('../fieldDecoratorMetadata');
29
34
 
30
35
  fieldsAreDistinct = Fields !== otherFields.Fields;
31
36
  derivedTypeIsDistinct = DerivedType !== otherDerivedType.DerivedType;
37
+ standardMetadataCopiesAreDistinct = standardMetadataWriter.addFieldToDecoratorMetadata !== addFieldToDecoratorMetadata &&
38
+ standardMetadataReader.getOwnDecoratorFieldsForType !== getOwnDecoratorFieldsForType &&
39
+ standardMetadataWriter.addFieldToDecoratorMetadata !== standardMetadataReader.addFieldToDecoratorMetadata;
32
40
 
33
41
  class Subject { }
34
42
 
35
43
  Fields.addFieldToType(Subject, 'name', String, false, [], []);
36
44
  DerivedType.set(Subject, 'an-identifier');
37
45
 
46
+ class StandardSubject { }
47
+ const metadata: Record<PropertyKey, unknown> = Object.create(null) as Record<PropertyKey, unknown>;
48
+ standardMetadataWriter.addFieldToDecoratorMetadata(metadata, new Field('standardName', String, false, [], []));
49
+ Object.defineProperty(StandardSubject, Symbol.metadata, { value: metadata });
50
+
38
51
  fieldNamesReadByTheOtherCopy = otherFields.Fields.getFieldsForType(Subject).map(_ => _.name);
39
52
  derivedTypeReadByTheOtherCopy = otherDerivedType.DerivedType.get(Subject);
53
+ standardFieldNamesReadByTheOtherCopy = standardMetadataReader.getOwnDecoratorFieldsForType(StandardSubject).map(_ => _.name);
54
+ standardFieldNamesReadByOtherFieldsCopy = otherFields.Fields.getFieldsForType(StandardSubject).map(_ => _.name);
40
55
  });
41
56
 
42
57
  it('should be a genuinely different Fields', () => fieldsAreDistinct.should.be.true);
43
58
  it('should be a genuinely different DerivedType', () => derivedTypeIsDistinct.should.be.true);
59
+ it('should use genuinely different standard metadata helpers', () => standardMetadataCopiesAreDistinct.should.be.true);
44
60
  it('should read a field the other copy wrote', () => fieldNamesReadByTheOtherCopy.should.deep.equal(['name']));
45
61
  it('should read a derived type the other copy wrote', () => derivedTypeReadByTheOtherCopy!.should.equal('an-identifier'));
62
+ it('should read a standard field another metadata helper copy wrote', () => standardFieldNamesReadByTheOtherCopy.should.deep.equal(['standardName']));
63
+ it('should expose standard metadata through another Fields copy', () => standardFieldNamesReadByOtherFieldsCopy.should.deep.equal(['standardName']));
46
64
  });
@@ -0,0 +1,28 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Constructor } from '../Constructor';
5
+ import { DerivedType } from '../DerivedType';
6
+ import { derivedType } from '../derivedTypeDecorator';
7
+
8
+ class BaseType { }
9
+ class OriginalType extends BaseType { }
10
+ class ReplacementType extends OriginalType { }
11
+
12
+ describe('when an outer standard decorator replaces the class', () => {
13
+ const initializers: Array<(this: Constructor) => void> = [];
14
+
15
+ derivedType('replacement-id')(OriginalType, {
16
+ addInitializer(initializer) {
17
+ initializers.push(initializer);
18
+ },
19
+ kind: 'class',
20
+ metadata: {},
21
+ name: 'OriginalType'
22
+ });
23
+ initializers.forEach(initializer => initializer.call(ReplacementType));
24
+
25
+ it('should not register the discarded original class', () => (DerivedType.get(OriginalType) === undefined).should.be.true);
26
+ it('should register the final replacement class', () => DerivedType.get(ReplacementType).should.equal('replacement-id'));
27
+ it('should register the replacement with its base type', () => DerivedType.getDerivedTypesFor(BaseType).should.deep.equal([ReplacementType]));
28
+ });
@@ -0,0 +1,76 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ declare const fundamentals: typeof import('../../index');
5
+
6
+ const { derivedType, field, JsonSerializer, ValueMap } = fundamentals;
7
+
8
+ function replaceClass<Target extends import('../../Constructor').Constructor>(target: Target): Target {
9
+ return class extends target { } as Target;
10
+ }
11
+
12
+ class Shape {
13
+ @field(String)
14
+ label!: string;
15
+ }
16
+
17
+ @derivedType('circle')
18
+ class Circle extends Shape {
19
+ @field(Number)
20
+ radius!: number;
21
+ }
22
+
23
+ @derivedType('rectangle')
24
+ class Rectangle extends Shape {
25
+ @field(Number)
26
+ width!: number;
27
+
28
+ @field(Number)
29
+ height!: number;
30
+ }
31
+
32
+ @replaceClass
33
+ @derivedType('replacement')
34
+ class ReplacedShape extends Shape { }
35
+
36
+ class Drawing {
37
+ static instanceCount = 0;
38
+
39
+ @field(String)
40
+ title!: string;
41
+
42
+ @field(Date)
43
+ createdAt!: Date;
44
+
45
+ @field(Shape, true)
46
+ shapes!: Shape[];
47
+
48
+ @field(ValueMap, { genericArguments: [String, Number] })
49
+ scores!: InstanceType<typeof ValueMap<string, number>>;
50
+
51
+ constructor() {
52
+ Drawing.instanceCount++;
53
+ }
54
+ }
55
+
56
+ const instanceCountBeforeDeserialize = Drawing.instanceCount;
57
+ const drawing = JsonSerializer.deserialize(Drawing, JSON.stringify({
58
+ createdAt: '2026-08-16T09:30:00.000Z',
59
+ scores: {
60
+ first: 42
61
+ },
62
+ shapes: [{
63
+ _derivedTypeId: 'circle',
64
+ label: 'First',
65
+ radius: 12
66
+ }, {
67
+ _derivedTypeId: 'rectangle',
68
+ height: 4,
69
+ label: 'Second',
70
+ width: 8
71
+ }],
72
+ title: 'Standard decorators'
73
+ }));
74
+ const instanceCountAfterDeserialize = Drawing.instanceCount;
75
+
76
+ void [Circle, Rectangle, ReplacedShape, instanceCountBeforeDeserialize, drawing, instanceCountAfterDeserialize];
@@ -0,0 +1,109 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { readFileSync } from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { Script } from 'node:vm';
7
+ import { createProgram, getPreEmitDiagnostics, ModuleKind, ModuleResolutionKind, ScriptTarget, transpileModule } from 'typescript';
8
+ import * as fundamentals from '../index';
9
+
10
+ describe('when executing standard TypeScript decorator emit', () => {
11
+ const originalMetadataDescriptor = Object.getOwnPropertyDescriptor(Symbol, 'metadata');
12
+ let emittedJavaScript: string;
13
+ let symbolMetadataWasAbsent: boolean;
14
+ let symbolMetadataWasPolyfilled: boolean;
15
+ let executionResult: {
16
+ Circle: fundamentals.Constructor;
17
+ Drawing: fundamentals.Constructor;
18
+ Rectangle: fundamentals.Constructor;
19
+ ReplacedShape: fundamentals.Constructor;
20
+ Shape: fundamentals.Constructor;
21
+ drawing: {
22
+ createdAt: Date;
23
+ scores: fundamentals.ValueMap<string, number>;
24
+ shapes: object[];
25
+ title: string;
26
+ };
27
+ instanceCountAfterDeserialize: number;
28
+ instanceCountBeforeDeserialize: number;
29
+ };
30
+ let registeredDerivedTypes: fundamentals.Constructor[];
31
+ let legacySemanticDiagnosticCount: number;
32
+ let semanticDiagnosticCount: number;
33
+
34
+ beforeAll(async () => {
35
+ Reflect.deleteProperty(Symbol, 'metadata');
36
+ symbolMetadataWasAbsent = !Object.prototype.hasOwnProperty.call(Symbol, 'metadata');
37
+
38
+ await import(/* @vite-ignore */ '../reflection?standardTypeScriptEmit');
39
+ symbolMetadataWasPolyfilled = Symbol.metadata === Symbol.for('Symbol.metadata');
40
+
41
+ const fixtureUrl = new URL('./fixtures/standard_decorated_types.ts', import.meta.url);
42
+ const fixturePath = fileURLToPath(fixtureUrl);
43
+ const source = readFileSync(fixtureUrl, 'utf8');
44
+ const compilerOptions = {
45
+ lib: ['lib.esnext.d.ts', 'lib.dom.d.ts'],
46
+ module: ModuleKind.ES2022,
47
+ moduleResolution: ModuleResolutionKind.Bundler,
48
+ noEmit: true,
49
+ noImplicitAny: false,
50
+ skipLibCheck: true,
51
+ strict: true,
52
+ target: ScriptTarget.ES2022
53
+ };
54
+ const standardProgram = createProgram({
55
+ options: {
56
+ ...compilerOptions,
57
+ emitDecoratorMetadata: false,
58
+ experimentalDecorators: false,
59
+ },
60
+ rootNames: [fixturePath]
61
+ });
62
+ const legacyProgram = createProgram({
63
+ options: {
64
+ ...compilerOptions,
65
+ emitDecoratorMetadata: true,
66
+ experimentalDecorators: true,
67
+ },
68
+ rootNames: [fixturePath]
69
+ });
70
+ semanticDiagnosticCount = getPreEmitDiagnostics(standardProgram).length;
71
+ legacySemanticDiagnosticCount = getPreEmitDiagnostics(legacyProgram).length;
72
+ emittedJavaScript = transpileModule(source, {
73
+ compilerOptions: {
74
+ emitDecoratorMetadata: false,
75
+ experimentalDecorators: false,
76
+ module: ModuleKind.None,
77
+ target: ScriptTarget.ES2022
78
+ },
79
+ fileName: 'standard_decorated_types.ts',
80
+ reportDiagnostics: true
81
+ }).outputText;
82
+
83
+ const script = new Script(`${emittedJavaScript}\n({ Circle, Drawing, Rectangle, ReplacedShape, Shape, drawing, instanceCountAfterDeserialize, instanceCountBeforeDeserialize })`);
84
+ executionResult = script.runInNewContext({ Array, Boolean, Date, fundamentals, JSON, Number, Object, String, Symbol }) as typeof executionResult;
85
+ registeredDerivedTypes = fundamentals.DerivedType.getDerivedTypesFor(executionResult.Shape);
86
+ });
87
+
88
+ afterAll(() => {
89
+ Reflect.deleteProperty(Symbol, 'metadata');
90
+ if (originalMetadataDescriptor) Object.defineProperty(Symbol, 'metadata', originalMetadataDescriptor);
91
+ });
92
+
93
+ it('should begin without runtime Symbol metadata support', () => symbolMetadataWasAbsent.should.be.true);
94
+ it('should install the Symbol metadata polyfill before class evaluation', () => symbolMetadataWasPolyfilled.should.be.true);
95
+ it('should type check the standard decorator signatures', () => semanticDiagnosticCount.should.equal(0));
96
+ it('should type check the legacy decorator signatures', () => legacySemanticDiagnosticCount.should.equal(0));
97
+ it('should execute standard decorator helper emit', () => emittedJavaScript.should.contain('__esDecorate'));
98
+ it('should expose field metadata before constructing an instance', () => executionResult.instanceCountBeforeDeserialize.should.equal(0));
99
+ it('should construct the target only during first-call deserialization', () => executionResult.instanceCountAfterDeserialize.should.equal(1));
100
+ it('should deserialize primitive fields', () => executionResult.drawing.title.should.equal('Standard decorators'));
101
+ it('should deserialize date fields', () => executionResult.drawing.createdAt.should.be.instanceOf(Date));
102
+ it('should deserialize polymorphic array entries', () => (executionResult.drawing.shapes[0] instanceof executionResult.Circle).should.be.true);
103
+ it('should deserialize every registered derivative', () => (executionResult.drawing.shapes[1] instanceof executionResult.Rectangle).should.be.true);
104
+ it('should auto-register standard derived type decorators', () => registeredDerivedTypes.should.deep.equal([executionResult.Circle, executionResult.Rectangle, executionResult.ReplacedShape]));
105
+ it('should register the final class returned by an outer standard decorator', () => fundamentals.DerivedType.get(executionResult.ReplacedShape).should.equal('replacement'));
106
+ it('should deserialize inherited fields', () => (executionResult.drawing.shapes[0] as { label: string }).label.should.equal('First'));
107
+ it('should deserialize derived fields', () => (executionResult.drawing.shapes[0] as { radius: number }).radius.should.equal(12));
108
+ it('should deserialize generic arguments', () => executionResult.drawing.scores.get('first')!.should.equal(42));
109
+ });
@@ -0,0 +1,21 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Fields } from '../Fields';
5
+ import { field } from '../fieldDecorator';
6
+
7
+ class FirstDerivative { }
8
+ class SecondDerivative { }
9
+ class LegacyDecoratedType { }
10
+
11
+ describe('when invoked as a legacy decorator', () => {
12
+ field(Number)(LegacyDecoratedType.prototype, 'count');
13
+ field(String, true, [FirstDerivative, SecondDerivative], [String, Number])(LegacyDecoratedType.prototype, 'values');
14
+
15
+ const fields = Fields.getFieldsForType(LegacyDecoratedType);
16
+
17
+ it('should register a primitive field', () => fields.find(_ => _.name === 'count')!.type.should.equal(Number));
18
+ it('should preserve enumerable semantics', () => fields.find(_ => _.name === 'values')!.enumerable.should.be.true);
19
+ it('should preserve derivative semantics', () => fields.find(_ => _.name === 'values')!.derivatives.should.deep.equal([FirstDerivative, SecondDerivative]));
20
+ it('should preserve generic argument semantics', () => fields.find(_ => _.name === 'values')!.genericArguments.should.deep.equal([String, Number]));
21
+ });
@@ -0,0 +1,43 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Fields } from '../Fields';
5
+ import { field } from '../fieldDecorator';
6
+ import { StandardFieldDecoratorContext } from '../StandardFieldDecoratorContext';
7
+
8
+ class FirstDerivative { }
9
+ class SecondDerivative { }
10
+
11
+ class StandardDecoratedType {
12
+ static instanceCount = 0;
13
+
14
+ constructor() {
15
+ StandardDecoratedType.instanceCount++;
16
+ }
17
+ }
18
+
19
+ describe('when invoked as a standard decorator', () => {
20
+ const metadata: Record<PropertyKey, unknown> = Object.create(null) as Record<PropertyKey, unknown>;
21
+ const contextFor = (name: string): StandardFieldDecoratorContext => ({
22
+ kind: 'field',
23
+ metadata,
24
+ name,
25
+ private: false,
26
+ static: false
27
+ });
28
+
29
+ field(Number)(undefined, contextFor('count'));
30
+ field(Date)(undefined, contextFor('createdAt'));
31
+ field(String, true)(undefined, contextFor('names'));
32
+ field(Object, { derivatives: [FirstDerivative, SecondDerivative], genericArguments: [String, Number] })(undefined, contextFor('value'));
33
+ Object.defineProperty(StandardDecoratedType, Symbol.metadata, { value: metadata });
34
+
35
+ const fields = Fields.getFieldsForType(StandardDecoratedType);
36
+
37
+ it('should expose metadata before constructing an instance', () => StandardDecoratedType.instanceCount.should.equal(0));
38
+ it('should register a primitive field', () => fields.find(_ => _.name === 'count')!.type.should.equal(Number));
39
+ it('should register a date field', () => fields.find(_ => _.name === 'createdAt')!.type.should.equal(Date));
40
+ it('should register an array field', () => fields.find(_ => _.name === 'names')!.enumerable.should.be.true);
41
+ it('should register derivatives', () => fields.find(_ => _.name === 'value')!.derivatives.should.deep.equal([FirstDerivative, SecondDerivative]));
42
+ it('should register generic arguments', () => fields.find(_ => _.name === 'value')!.genericArguments.should.deep.equal([String, Number]));
43
+ });
@@ -0,0 +1,12 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { field } from '../../fieldDecorator';
5
+
6
+ class Subject { }
7
+
8
+ describe('when invoked for an unsupported legacy field with a static field', () => {
9
+ const invoke = () => field(String)(Subject, 'value');
10
+
11
+ it('should reject the field', () => invoke.should.throw(TypeError, '@field cannot decorate static fields'));
12
+ });
@@ -0,0 +1,12 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { field } from '../../fieldDecorator';
5
+
6
+ class Subject { }
7
+
8
+ describe('when invoked for an unsupported legacy field with a symbol name', () => {
9
+ const invoke = () => field(String)(Subject.prototype, Symbol('value'));
10
+
11
+ it('should reject the field', () => invoke.should.throw(TypeError, '@field cannot decorate symbol-named fields'));
12
+ });
@@ -0,0 +1,16 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { field } from '../../fieldDecorator';
5
+
6
+ describe('when invoked for an unsupported standard field with a private field', () => {
7
+ const invoke = () => field(String)(undefined, {
8
+ kind: 'field',
9
+ metadata: {},
10
+ name: '#value',
11
+ private: true,
12
+ static: false
13
+ });
14
+
15
+ it('should reject the field', () => invoke.should.throw(TypeError, '@field cannot decorate private fields'));
16
+ });
@@ -0,0 +1,16 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { field } from '../../fieldDecorator';
5
+
6
+ describe('when invoked for an unsupported standard field with a static field', () => {
7
+ const invoke = () => field(String)(undefined, {
8
+ kind: 'field',
9
+ metadata: {},
10
+ name: 'value',
11
+ private: false,
12
+ static: true
13
+ });
14
+
15
+ it('should reject the field', () => invoke.should.throw(TypeError, '@field cannot decorate static fields'));
16
+ });
@@ -0,0 +1,16 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { field } from '../../fieldDecorator';
5
+
6
+ describe('when invoked for an unsupported standard field with a symbol name', () => {
7
+ const invoke = () => field(String)(undefined, {
8
+ kind: 'field',
9
+ metadata: {},
10
+ name: Symbol('value'),
11
+ private: false,
12
+ static: false
13
+ });
14
+
15
+ it('should reject the field', () => invoke.should.throw(TypeError, '@field cannot decorate symbol-named fields'));
16
+ });
@@ -0,0 +1,16 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { field } from '../../fieldDecorator';
5
+
6
+ describe('when invoked for an unsupported standard field without decorator metadata', () => {
7
+ const invoke = () => field(String)(undefined, {
8
+ kind: 'field',
9
+ metadata: undefined,
10
+ name: 'value',
11
+ private: false,
12
+ static: false
13
+ });
14
+
15
+ it('should reject the field', () => invoke.should.throw(TypeError, '@field requires standard decorator metadata support'));
16
+ });
@@ -0,0 +1,43 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Fields } from '../Fields';
5
+ import { field } from '../fieldDecorator';
6
+ import { StandardFieldDecoratorContext } from '../StandardFieldDecoratorContext';
7
+
8
+ class LegacyBase { }
9
+ class StandardDerived extends LegacyBase { }
10
+ class StandardBase { }
11
+ class LegacyDerived extends StandardBase { }
12
+
13
+ describe('when legacy and standard metadata are mixed across inheritance', () => {
14
+ const standardDerivedMetadata: Record<PropertyKey, unknown> = Object.create(null) as Record<PropertyKey, unknown>;
15
+ const standardBaseMetadata: Record<PropertyKey, unknown> = Object.create(null) as Record<PropertyKey, unknown>;
16
+ const contextFor = (name: string, metadata: Record<PropertyKey, unknown>): StandardFieldDecoratorContext => ({
17
+ kind: 'field',
18
+ metadata,
19
+ name,
20
+ private: false,
21
+ static: false
22
+ });
23
+
24
+ field(String)(LegacyBase.prototype, 'value');
25
+ field(String)(LegacyBase.prototype, 'legacyBaseValue');
26
+ field(Number)(undefined, contextFor('value', standardDerivedMetadata));
27
+ field(Number)(undefined, contextFor('standardDerivedValue', standardDerivedMetadata));
28
+ Object.defineProperty(StandardDerived, Symbol.metadata, { value: standardDerivedMetadata });
29
+
30
+ field(String)(undefined, contextFor('value', standardBaseMetadata));
31
+ field(String)(undefined, contextFor('standardBaseValue', standardBaseMetadata));
32
+ Object.defineProperty(StandardBase, Symbol.metadata, { value: standardBaseMetadata });
33
+ field(Boolean)(LegacyDerived.prototype, 'value');
34
+ field(Boolean)(LegacyDerived.prototype, 'legacyDerivedValue');
35
+
36
+ const standardDerivedFields = Fields.getFieldsForType(StandardDerived);
37
+ const legacyDerivedFields = Fields.getFieldsForType(LegacyDerived);
38
+
39
+ it('should inherit legacy fields into a standard derived type', () => standardDerivedFields.find(_ => _.name === 'legacyBaseValue')!.type.should.equal(String));
40
+ it('should let a standard field override a legacy base field', () => standardDerivedFields.find(_ => _.name === 'value')!.type.should.equal(Number));
41
+ it('should inherit standard fields into a legacy derived type', () => legacyDerivedFields.find(_ => _.name === 'standardBaseValue')!.type.should.equal(String));
42
+ it('should let a legacy field override a standard base field', () => legacyDerivedFields.find(_ => _.name === 'value')!.type.should.equal(Boolean));
43
+ });
@@ -0,0 +1,34 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Field } from '../Field';
5
+ import { Fields } from '../Fields';
6
+ import { field } from '../fieldDecorator';
7
+
8
+ describe('when standard metadata contains unrelated fields metadata', () => {
9
+ let fields: Field[];
10
+ let metadata: Record<PropertyKey, unknown>;
11
+ let unrelatedMetadata: object;
12
+
13
+ beforeEach(() => {
14
+ class Subject { }
15
+
16
+ metadata = Object.create(null) as Record<PropertyKey, unknown>;
17
+ unrelatedMetadata = { source: 'another library' };
18
+ Reflect.defineMetadata('fields', unrelatedMetadata, metadata);
19
+
20
+ field(String)(undefined, {
21
+ kind: 'field',
22
+ metadata,
23
+ name: 'name',
24
+ private: false,
25
+ static: false
26
+ });
27
+ Object.defineProperty(Subject, Symbol.metadata, { value: metadata });
28
+ fields = Fields.getFieldsForType(Subject);
29
+ });
30
+
31
+ it('should preserve the unrelated fields metadata', () => Reflect.getOwnMetadata('fields', metadata).should.equal(unrelatedMetadata));
32
+ it('should register the decorated field independently', () => fields.map(_ => _.name).should.deep.equal(['name']));
33
+ it('should retain the decorated field type', () => fields[0].type.should.equal(String));
34
+ });
@@ -0,0 +1,37 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Fields } from '../Fields';
5
+ import { field } from '../fieldDecorator';
6
+ import { StandardFieldDecoratorContext } from '../StandardFieldDecoratorContext';
7
+
8
+ class BaseType { }
9
+ class DerivedType extends BaseType { }
10
+
11
+ describe('when standard metadata is inherited and overridden', () => {
12
+ const baseMetadata: Record<PropertyKey, unknown> = Object.create(null) as Record<PropertyKey, unknown>;
13
+ const derivedMetadata: Record<PropertyKey, unknown> = Object.create(baseMetadata) as Record<PropertyKey, unknown>;
14
+ const contextFor = (name: string, metadata: Record<PropertyKey, unknown>): StandardFieldDecoratorContext => ({
15
+ kind: 'field',
16
+ metadata,
17
+ name,
18
+ private: false,
19
+ static: false
20
+ });
21
+
22
+ field(String)(undefined, contextFor('baseValue', baseMetadata));
23
+ field(String)(undefined, contextFor('value', baseMetadata));
24
+ field(Number)(undefined, contextFor('value', derivedMetadata));
25
+ field(Boolean)(undefined, contextFor('derivedValue', derivedMetadata));
26
+ Object.defineProperty(BaseType, Symbol.metadata, { value: baseMetadata });
27
+ Object.defineProperty(DerivedType, Symbol.metadata, { value: derivedMetadata });
28
+
29
+ const baseFields = Fields.getFieldsForType(BaseType);
30
+ const derivedFields = Fields.getFieldsForType(DerivedType);
31
+
32
+ it('should preserve fields declared by the base type', () => derivedFields.find(_ => _.name === 'baseValue')!.type.should.equal(String));
33
+ it('should preserve fields declared by the derived type', () => derivedFields.find(_ => _.name === 'derivedValue')!.type.should.equal(Boolean));
34
+ it('should keep a single overridden field', () => derivedFields.filter(_ => _.name === 'value').should.have.lengthOf(1));
35
+ it('should use the derived field definition', () => derivedFields.find(_ => _.name === 'value')!.type.should.equal(Number));
36
+ it('should not mutate the base field definition', () => baseFields.find(_ => _.name === 'value')!.type.should.equal(String));
37
+ });
@@ -0,0 +1,25 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ describe('when loading with existing Symbol metadata', () => {
5
+ const originalMetadataDescriptor = Object.getOwnPropertyDescriptor(Symbol, 'metadata');
6
+ const existingMetadata = Symbol('existing metadata');
7
+ let actualMetadata: symbol;
8
+
9
+ beforeAll(async () => {
10
+ Object.defineProperty(Symbol, 'metadata', {
11
+ configurable: true,
12
+ value: existingMetadata
13
+ });
14
+
15
+ await import(/* @vite-ignore */ '../reflection?existingSymbolMetadata');
16
+ actualMetadata = Symbol.metadata;
17
+ });
18
+
19
+ afterAll(() => {
20
+ Reflect.deleteProperty(Symbol, 'metadata');
21
+ if (originalMetadataDescriptor) Object.defineProperty(Symbol, 'metadata', originalMetadataDescriptor);
22
+ });
23
+
24
+ it('should preserve the existing identity', () => actualMetadata.should.equal(existingMetadata));
25
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/fundamentals",
3
- "version": "7.17.0",
3
+ "version": "7.18.0",
4
4
  "description": "",
5
5
  "author": "Cratis",
6
6
  "license": "MIT",
package/reflection.ts CHANGED
@@ -29,6 +29,19 @@ declare global {
29
29
  }
30
30
  }
31
31
 
32
+ // Standard decorators expose their class metadata through Symbol.metadata. Define it before any
33
+ // decorated class is evaluated on runtimes that do not provide it yet. Symbol.for keeps the value
34
+ // stable if more than one copy of this package is loaded in the same realm.
35
+ const symbolConstructor = Symbol as unknown as { metadata?: unknown };
36
+ if (symbolConstructor.metadata === undefined || symbolConstructor.metadata === null) {
37
+ Object.defineProperty(Symbol, 'metadata', {
38
+ configurable: true,
39
+ enumerable: false,
40
+ value: Symbol.for('Symbol.metadata'),
41
+ writable: false
42
+ });
43
+ }
44
+
32
45
  // Check if native metadata API is available by testing for multiple methods
33
46
  if (typeof Reflect.defineMetadata !== 'function' || typeof Reflect.getOwnMetadata !== 'function') {
34
47
  const metadataMap = new WeakMap<any, Map<string | symbol, any>>();