@auto-engineer/server-generator-apollo-emmett 1.146.0 → 1.148.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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +6 -6
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +68 -0
- package/dist/src/codegen/extract/messages.d.ts.map +1 -1
- package/dist/src/codegen/extract/messages.js +4 -12
- package/dist/src/codegen/extract/messages.js.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.d.ts +1 -11
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +7 -38
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templateHelpers.d.ts +11 -0
- package/dist/src/codegen/templateHelpers.d.ts.map +1 -0
- package/dist/src/codegen/templateHelpers.js +47 -0
- package/dist/src/codegen/templateHelpers.js.map +1 -0
- package/dist/src/codegen/templates/command/decide.specs.specs.ts +662 -0
- package/dist/src/codegen/templates/command/decide.specs.ts +1 -10
- package/dist/src/codegen/templates/command/decide.specs.ts.ejs +3 -47
- package/dist/src/codegen/templates/command/decide.ts.ejs +31 -2
- package/dist/src/codegen/templates/command/handle.specs.ts +0 -201
- package/dist/src/codegen/templates/command/handle.ts.ejs +0 -34
- package/dist/src/codegen/templates/command/state.specs.ts +89 -0
- package/dist/src/codegen/templates/command/state.ts.ejs +20 -0
- package/dist/src/codegen/templates/query/events.specs.ts +133 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +5 -4
- package/package.json +4 -4
- package/src/codegen/extract/messages.specs.ts +9 -8
- package/src/codegen/extract/messages.ts +4 -12
- package/src/codegen/extract/states.specs.ts +53 -0
- package/src/codegen/scaffoldFromSchema.ts +7 -58
- package/src/codegen/templateHelpers.specs.ts +98 -0
- package/src/codegen/templateHelpers.ts +58 -0
- package/src/codegen/templates/command/decide.specs.specs.ts +662 -0
- package/src/codegen/templates/command/decide.specs.ts +1 -10
- package/src/codegen/templates/command/decide.specs.ts.ejs +3 -47
- package/src/codegen/templates/command/decide.ts.ejs +31 -2
- package/src/codegen/templates/command/handle.specs.ts +0 -201
- package/src/codegen/templates/command/handle.ts.ejs +0 -34
- package/src/codegen/templates/command/state.specs.ts +89 -0
- package/src/codegen/templates/command/state.ts.ejs +20 -0
- package/src/codegen/templates/query/events.specs.ts +133 -0
- package/src/codegen/buildCrossSceneGivens.specs.ts +0 -263
package/ketchup-plan.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
# Ketchup Plan:
|
|
1
|
+
# Ketchup Plan: G4 — Fix remaining decide.ts implementer failures
|
|
2
2
|
|
|
3
3
|
## TODO
|
|
4
4
|
|
|
5
5
|
## DONE
|
|
6
6
|
|
|
7
|
-
- [x]
|
|
8
|
-
- [x]
|
|
9
|
-
- [x]
|
|
7
|
+
- [x] B1: Extract shared template helpers and wire into template data [depends: none] (78f159a4)
|
|
8
|
+
- [x] B3: Add hasGivenStates + state context instruction to decide.ts.ejs [depends: B1] (42f66c71)
|
|
9
|
+
- [x] B4: Add context-aware nonCommandField classification to decide.ts.ejs [depends: B3] (65212e33)
|
|
10
|
+
- [x] B5: Add Given state ref hints to state.ts.ejs [depends: none]
|
package/package.json
CHANGED
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"uuid": "^13.0.0",
|
|
33
33
|
"web-streams-polyfill": "^4.1.0",
|
|
34
34
|
"zod": "^3.22.4",
|
|
35
|
-
"@auto-engineer/narrative": "1.
|
|
36
|
-
"@auto-engineer/message-bus": "1.
|
|
35
|
+
"@auto-engineer/narrative": "1.148.0",
|
|
36
|
+
"@auto-engineer/message-bus": "1.148.0"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"typescript": "^5.8.3",
|
|
45
45
|
"vitest": "^3.2.4",
|
|
46
46
|
"tsx": "^4.19.2",
|
|
47
|
-
"@auto-engineer/cli": "1.
|
|
47
|
+
"@auto-engineer/cli": "1.148.0"
|
|
48
48
|
},
|
|
49
|
-
"version": "1.
|
|
49
|
+
"version": "1.148.0",
|
|
50
50
|
"scripts": {
|
|
51
51
|
"generate:server": "tsx src/cli/index.ts",
|
|
52
52
|
"build": "tsc && tsx ../../scripts/fix-esm-imports.ts && rm -rf dist/src/codegen/templates && mkdir -p dist/src/codegen && cp -r src/codegen/templates dist/src/codegen/templates && cp src/server.ts dist/src && cp -r src/utils dist/src && cp -r src/domain dist/src",
|
|
@@ -190,7 +190,7 @@ describe('extractMessagesFromSpecs (react slice)', () => {
|
|
|
190
190
|
});
|
|
191
191
|
|
|
192
192
|
describe('extractMessagesFromSpecs (command slice)', () => {
|
|
193
|
-
it('should
|
|
193
|
+
it('should extract Given-step state refs into states array, not events', () => {
|
|
194
194
|
const slice: Moment = {
|
|
195
195
|
type: 'command',
|
|
196
196
|
name: 'book barber appointment',
|
|
@@ -263,23 +263,24 @@ describe('extractMessagesFromSpecs (command slice)', () => {
|
|
|
263
263
|
|
|
264
264
|
expect(result.events).toEqual([
|
|
265
265
|
{
|
|
266
|
-
type: '
|
|
266
|
+
type: 'BarberAppointmentBooked',
|
|
267
267
|
fields: [
|
|
268
268
|
{ name: 'customerId', tsType: 'string', required: true },
|
|
269
|
-
{ name: '
|
|
269
|
+
{ name: 'barberId', tsType: 'string', required: true },
|
|
270
270
|
],
|
|
271
271
|
source: 'then',
|
|
272
|
+
sourceSceneName: undefined,
|
|
272
273
|
sourceMomentName: 'book barber appointment',
|
|
273
274
|
},
|
|
275
|
+
]);
|
|
276
|
+
|
|
277
|
+
expect(result.states).toEqual([
|
|
274
278
|
{
|
|
275
|
-
type: '
|
|
279
|
+
type: 'CustomerAppointments',
|
|
276
280
|
fields: [
|
|
277
281
|
{ name: 'customerId', tsType: 'string', required: true },
|
|
278
|
-
{ name: '
|
|
282
|
+
{ name: 'appointments', tsType: 'object[]', required: true },
|
|
279
283
|
],
|
|
280
|
-
source: 'then',
|
|
281
|
-
sourceSceneName: undefined,
|
|
282
|
-
sourceMomentName: 'book barber appointment',
|
|
283
284
|
},
|
|
284
285
|
]);
|
|
285
286
|
});
|
|
@@ -91,16 +91,8 @@ function extractMessagesForCommand(slice: Moment, allMessages: MessageDefinition
|
|
|
91
91
|
debugCommand(' Command schemas: %o', Object.keys(commandSchemasByName));
|
|
92
92
|
|
|
93
93
|
const allGivenRefs = gwtSpecs.flatMap((gwt) => gwt.given);
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
.filter((ref) => allMessages.some((m) => m.type === 'state' && m.name === ref.eventRef))
|
|
97
|
-
.map((ref) => ({
|
|
98
|
-
type: ref.eventRef,
|
|
99
|
-
fields: extractFieldsFromMessage(ref.eventRef, 'state', allMessages),
|
|
100
|
-
source: 'then' as const,
|
|
101
|
-
sourceMomentName: slice.name,
|
|
102
|
-
}));
|
|
103
|
-
debugCommand(' State-as-events from Given: %d', stateAsEvents.length);
|
|
94
|
+
const givenStates = extractStatesFromGiven(allGivenRefs, allMessages);
|
|
95
|
+
debugCommand(' Given states: %d', givenStates.length);
|
|
104
96
|
|
|
105
97
|
const events: Message[] = gwtSpecs.flatMap((gwt): Message[] => {
|
|
106
98
|
const eventOnlyGiven = gwt.given.filter((ref) =>
|
|
@@ -120,8 +112,8 @@ function extractMessagesForCommand(slice: Moment, allMessages: MessageDefinition
|
|
|
120
112
|
|
|
121
113
|
const result = {
|
|
122
114
|
commands,
|
|
123
|
-
events: deduplicateMessages([...
|
|
124
|
-
states:
|
|
115
|
+
events: deduplicateMessages([...events, ...dataTargetEvents]),
|
|
116
|
+
states: givenStates,
|
|
125
117
|
commandSchemasByName,
|
|
126
118
|
};
|
|
127
119
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { EventRef, MessageDefinition } from '../types';
|
|
3
|
+
import { extractStatesFromGiven } from './states';
|
|
4
|
+
|
|
5
|
+
describe('extractStatesFromGiven', () => {
|
|
6
|
+
it('should extract state refs from given and return state messages', () => {
|
|
7
|
+
const givenRefs: EventRef[] = [{ eventRef: 'CustomerAppointments' }, { eventRef: 'OrderPlaced' }];
|
|
8
|
+
|
|
9
|
+
const allMessages: MessageDefinition[] = [
|
|
10
|
+
{
|
|
11
|
+
type: 'state',
|
|
12
|
+
name: 'CustomerAppointments',
|
|
13
|
+
fields: [
|
|
14
|
+
{ name: 'customerId', type: 'string', required: true },
|
|
15
|
+
{ name: 'appointments', type: 'object[]', required: true },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
type: 'event',
|
|
20
|
+
name: 'OrderPlaced',
|
|
21
|
+
fields: [{ name: 'orderId', type: 'string', required: true }],
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const result = extractStatesFromGiven(givenRefs, allMessages);
|
|
26
|
+
|
|
27
|
+
expect(result).toEqual([
|
|
28
|
+
{
|
|
29
|
+
type: 'CustomerAppointments',
|
|
30
|
+
fields: [
|
|
31
|
+
{ name: 'customerId', tsType: 'string', required: true },
|
|
32
|
+
{ name: 'appointments', tsType: 'object[]', required: true },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should return empty array when no given refs match state messages', () => {
|
|
39
|
+
const givenRefs: EventRef[] = [{ eventRef: 'OrderPlaced' }];
|
|
40
|
+
|
|
41
|
+
const allMessages: MessageDefinition[] = [
|
|
42
|
+
{
|
|
43
|
+
type: 'event',
|
|
44
|
+
name: 'OrderPlaced',
|
|
45
|
+
fields: [{ name: 'orderId', type: 'string', required: true }],
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const result = extractStatesFromGiven(givenRefs, allMessages);
|
|
50
|
+
|
|
51
|
+
expect(result).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -42,11 +42,12 @@ import {
|
|
|
42
42
|
isValidTsIdentifier,
|
|
43
43
|
sanitizeFieldType,
|
|
44
44
|
} from './extract';
|
|
45
|
-
import {
|
|
45
|
+
import { getStreamFromSink } from './extract/data-sink';
|
|
46
46
|
import { buildEventIdFieldMap } from './extract/projection';
|
|
47
47
|
import { buildArgToStateFieldMap } from './extract/query';
|
|
48
48
|
import { normalizeMomentForTemplate } from './extract/slice-normalizer';
|
|
49
49
|
import { extractGwtSpecsFromMoment, type GwtResult } from './extract/step-converter';
|
|
50
|
+
import { buildKeepFieldNames, findDerivedDateInfo, isKeyTraceable } from './templateHelpers';
|
|
50
51
|
import type { GwtCondition, Message, MessageDefinition } from './types';
|
|
51
52
|
|
|
52
53
|
export class TemplateRenderError extends Error {
|
|
@@ -431,6 +432,9 @@ async function renderTemplate(
|
|
|
431
432
|
isReferencedMessageTypeArray,
|
|
432
433
|
extractReferencedTypeName,
|
|
433
434
|
referencedTypes: data.referencedTypes,
|
|
435
|
+
findDerivedDateInfo,
|
|
436
|
+
isKeyTraceable,
|
|
437
|
+
buildKeepFieldNames,
|
|
434
438
|
});
|
|
435
439
|
|
|
436
440
|
debugTemplate('Template rendered, output size: %d bytes', result.length);
|
|
@@ -709,8 +713,6 @@ async function prepareTemplateData(
|
|
|
709
713
|
const argToStateFieldMap =
|
|
710
714
|
slice.type === 'query' ? buildArgToStateFieldMap(queryGwtMapping, stateFieldNames, slice.mappings) : undefined;
|
|
711
715
|
|
|
712
|
-
const crossSceneGivens = buildCrossSceneGivens(gwtMapping, events, scene, scenes ?? [], filteredCommands);
|
|
713
|
-
|
|
714
716
|
return {
|
|
715
717
|
sceneName: scene.name,
|
|
716
718
|
momentName: slice.name,
|
|
@@ -741,63 +743,9 @@ async function prepareTemplateData(
|
|
|
741
743
|
eventCommandPairs,
|
|
742
744
|
eventIdFieldMap,
|
|
743
745
|
argToStateFieldMap,
|
|
744
|
-
crossSceneGivens,
|
|
745
746
|
};
|
|
746
747
|
}
|
|
747
748
|
|
|
748
|
-
interface CrossSceneGiven {
|
|
749
|
-
sourceStreamPattern: string;
|
|
750
|
-
linkingFields: Array<{
|
|
751
|
-
streamVar: string;
|
|
752
|
-
commandField: string;
|
|
753
|
-
}>;
|
|
754
|
-
allVarsResolved: boolean;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
export function buildCrossSceneGivens(
|
|
758
|
-
gwtMapping: Record<string, GwtCondition[]>,
|
|
759
|
-
events: Message[],
|
|
760
|
-
currentScene: Scene,
|
|
761
|
-
scenes: Scene[],
|
|
762
|
-
filteredCommands: Message[],
|
|
763
|
-
): CrossSceneGiven[] {
|
|
764
|
-
const commandFieldNames = new Set(filteredCommands.flatMap((c) => c.fields.map((f) => f.name)));
|
|
765
|
-
const seenPatterns = new Set<string>();
|
|
766
|
-
const result: CrossSceneGiven[] = [];
|
|
767
|
-
|
|
768
|
-
for (const conditions of Object.values(gwtMapping)) {
|
|
769
|
-
for (const gwt of conditions) {
|
|
770
|
-
for (const givenRef of gwt.given ?? []) {
|
|
771
|
-
if (!('eventRef' in givenRef)) continue;
|
|
772
|
-
|
|
773
|
-
const event = events.find((e) => e.type === givenRef.eventRef);
|
|
774
|
-
if (!event || event.sourceSceneName === currentScene.name) continue;
|
|
775
|
-
|
|
776
|
-
const sourceScene = scenes.find((s) => s.name === event.sourceSceneName);
|
|
777
|
-
const sourceMoment = sourceScene?.moments.find((m) => m.name === event.sourceMomentName);
|
|
778
|
-
if (!sourceMoment) continue;
|
|
779
|
-
|
|
780
|
-
const { streamPattern } = getStreamFromSink(sourceMoment);
|
|
781
|
-
if (!streamPattern || seenPatterns.has(streamPattern)) continue;
|
|
782
|
-
seenPatterns.add(streamPattern);
|
|
783
|
-
|
|
784
|
-
const streamVars = extractStreamIdFields(streamPattern);
|
|
785
|
-
const linkingFields = streamVars
|
|
786
|
-
.filter((v) => commandFieldNames.has(v))
|
|
787
|
-
.map((v) => ({ streamVar: v, commandField: v }));
|
|
788
|
-
|
|
789
|
-
result.push({
|
|
790
|
-
sourceStreamPattern: streamPattern,
|
|
791
|
-
linkingFields,
|
|
792
|
-
allVarsResolved: linkingFields.length === streamVars.length,
|
|
793
|
-
});
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
return result;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
749
|
function annotateEventSources(
|
|
802
750
|
events: Message[],
|
|
803
751
|
scenes: Scene[],
|
|
@@ -980,11 +928,12 @@ async function generateFilesForMoment(
|
|
|
980
928
|
);
|
|
981
929
|
|
|
982
930
|
debugMoment(' Generating %d files from templates', filteredTemplates.length);
|
|
983
|
-
const
|
|
931
|
+
const allPlans = await Promise.all(
|
|
984
932
|
filteredTemplates.map((template) =>
|
|
985
933
|
generateFileForTemplate(template, slice, momentDir, templateData, unionToEnumName),
|
|
986
934
|
),
|
|
987
935
|
);
|
|
936
|
+
const plans = allPlans.filter((p) => p.contents.trim().length > 0);
|
|
988
937
|
debugMoment(' Generated %d file plans for moment: %s', plans.length, slice.name);
|
|
989
938
|
return { plans, duplicateCommands };
|
|
990
939
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildKeepFieldNames, findDerivedDateInfo, isKeyTraceable } from './templateHelpers';
|
|
3
|
+
|
|
4
|
+
describe('findDerivedDateInfo', () => {
|
|
5
|
+
it('should detect a date field not in command fields and not in given events', () => {
|
|
6
|
+
const eventResults = [{ exampleData: { workoutId: 'w1', date: '2024-01-01', duration: 30 } }];
|
|
7
|
+
const commandFieldNames = new Set(['workoutId', 'duration']);
|
|
8
|
+
|
|
9
|
+
const result = findDerivedDateInfo(eventResults, commandFieldNames, undefined);
|
|
10
|
+
|
|
11
|
+
expect(result).toEqual({ date: '2024-01-01', fields: ['date'] });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should return null date when no date-like fields exist', () => {
|
|
15
|
+
const eventResults = [{ exampleData: { workoutId: 'w1', duration: 30 } }];
|
|
16
|
+
const commandFieldNames = new Set(['workoutId', 'duration']);
|
|
17
|
+
|
|
18
|
+
const result = findDerivedDateInfo(eventResults, commandFieldNames, undefined);
|
|
19
|
+
|
|
20
|
+
expect(result).toEqual({ date: null, fields: [] });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should exclude date values that appear in given events', () => {
|
|
24
|
+
const eventResults = [{ exampleData: { id: 'x', startDate: '2024-01-01' } }];
|
|
25
|
+
const commandFieldNames = new Set(['id']);
|
|
26
|
+
const givenEvents = [{ exampleData: { startDate: '2024-01-01' } }];
|
|
27
|
+
|
|
28
|
+
const result = findDerivedDateInfo(eventResults, commandFieldNames, givenEvents);
|
|
29
|
+
|
|
30
|
+
expect(result).toEqual({ date: null, fields: [] });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should return null when multiple distinct dates exist', () => {
|
|
34
|
+
const eventResults = [{ exampleData: { startDate: '2024-01-01', endDate: '2024-02-01' } }];
|
|
35
|
+
const commandFieldNames = new Set<string>();
|
|
36
|
+
|
|
37
|
+
const result = findDerivedDateInfo(eventResults, commandFieldNames, undefined);
|
|
38
|
+
|
|
39
|
+
expect(result).toEqual({ date: null, fields: [] });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should group multiple fields sharing the same date', () => {
|
|
43
|
+
const eventResults = [{ exampleData: { createdAt: '2024-01-01', updatedAt: '2024-01-01' } }];
|
|
44
|
+
const commandFieldNames = new Set<string>();
|
|
45
|
+
|
|
46
|
+
const result = findDerivedDateInfo(eventResults, commandFieldNames, undefined);
|
|
47
|
+
|
|
48
|
+
expect(result).toEqual({ date: '2024-01-01', fields: ['createdAt', 'updatedAt'] });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('isKeyTraceable', () => {
|
|
53
|
+
it('should return true when key+value match a given event', () => {
|
|
54
|
+
const givenEvents = [{ exampleData: { userId: 'u1' } }];
|
|
55
|
+
|
|
56
|
+
expect(isKeyTraceable('userId', 'u1', givenEvents)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should return false when value does not match', () => {
|
|
60
|
+
const givenEvents = [{ exampleData: { userId: 'u1' } }];
|
|
61
|
+
|
|
62
|
+
expect(isKeyTraceable('userId', 'u2', givenEvents)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should return false for null/undefined/object values', () => {
|
|
66
|
+
expect(isKeyTraceable('x', null, [{ exampleData: { x: null } }])).toBe(false);
|
|
67
|
+
expect(isKeyTraceable('x', undefined, [{ exampleData: { x: undefined } }])).toBe(false);
|
|
68
|
+
expect(isKeyTraceable('x', { a: 1 }, [{ exampleData: { x: { a: 1 } } }])).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should return false when no given events', () => {
|
|
72
|
+
expect(isKeyTraceable('x', 'v', undefined)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('buildKeepFieldNames', () => {
|
|
77
|
+
it('should include command fields, derived date fields, and traceable fields', () => {
|
|
78
|
+
const eventResults = [{ exampleData: { workoutId: 'w1', userId: 'u1', date: '2024-01-01', duration: 30 } }];
|
|
79
|
+
const commandFieldNames = new Set(['workoutId', 'duration']);
|
|
80
|
+
const derivedDateFieldNames = ['date'];
|
|
81
|
+
const givenEvents = [{ exampleData: { userId: 'u1' } }];
|
|
82
|
+
|
|
83
|
+
const result = buildKeepFieldNames(eventResults, commandFieldNames, derivedDateFieldNames, givenEvents);
|
|
84
|
+
|
|
85
|
+
expect(result).toEqual(new Set(['workoutId', 'duration', 'date', 'userId']));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should not include untraceable fields', () => {
|
|
89
|
+
const eventResults = [{ exampleData: { id: 'x', mystery: 'abc' } }];
|
|
90
|
+
const commandFieldNames = new Set(['id']);
|
|
91
|
+
const derivedDateFieldNames: string[] = [];
|
|
92
|
+
const givenEvents: Array<{ exampleData?: Record<string, unknown> }> = [];
|
|
93
|
+
|
|
94
|
+
const result = buildKeepFieldNames(eventResults, commandFieldNames, derivedDateFieldNames, givenEvents);
|
|
95
|
+
|
|
96
|
+
expect(result).toEqual(new Set(['id']));
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
interface EventData {
|
|
2
|
+
exampleData?: Record<string, unknown>;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function findDerivedDateInfo(
|
|
6
|
+
eventResults: EventData[],
|
|
7
|
+
commandFieldNames: Set<string>,
|
|
8
|
+
givenEvents: EventData[] | undefined,
|
|
9
|
+
): { date: string | null; fields: string[] } {
|
|
10
|
+
const givenValues = new Set<string>();
|
|
11
|
+
for (const g of givenEvents ?? []) {
|
|
12
|
+
for (const val of Object.values(g.exampleData ?? {})) {
|
|
13
|
+
if (typeof val === 'string') givenValues.add(val);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const fieldsByDate = new Map<string, string[]>();
|
|
17
|
+
for (const e of eventResults) {
|
|
18
|
+
for (const [key, val] of Object.entries(e.exampleData ?? {})) {
|
|
19
|
+
if (
|
|
20
|
+
!commandFieldNames.has(key) &&
|
|
21
|
+
typeof val === 'string' &&
|
|
22
|
+
/^\d{4}-\d{2}-\d{2}$/.test(val) &&
|
|
23
|
+
!givenValues.has(val)
|
|
24
|
+
) {
|
|
25
|
+
if (!fieldsByDate.has(val)) fieldsByDate.set(val, []);
|
|
26
|
+
fieldsByDate.get(val)!.push(key);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (fieldsByDate.size !== 1) return { date: null, fields: [] };
|
|
31
|
+
const [date, fields] = [...fieldsByDate.entries()][0];
|
|
32
|
+
return { date, fields };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isKeyTraceable(key: string, value: unknown, givenEvents: EventData[] | undefined): boolean {
|
|
36
|
+
if (value === null || value === undefined || typeof value === 'object') return false;
|
|
37
|
+
for (const g of givenEvents ?? []) {
|
|
38
|
+
if ((g.exampleData ?? {})[key] === value) return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildKeepFieldNames(
|
|
44
|
+
eventResults: EventData[],
|
|
45
|
+
commandFieldNames: Set<string>,
|
|
46
|
+
derivedDateFieldNames: string[],
|
|
47
|
+
givenEvents: EventData[] | undefined,
|
|
48
|
+
): Set<string> {
|
|
49
|
+
const keep = new Set([...commandFieldNames, ...derivedDateFieldNames]);
|
|
50
|
+
for (const e of eventResults) {
|
|
51
|
+
for (const [key, value] of Object.entries(e.exampleData ?? {})) {
|
|
52
|
+
if (!keep.has(key) && isKeyTraceable(key, value, givenEvents)) {
|
|
53
|
+
keep.add(key);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return keep;
|
|
58
|
+
}
|