@auto-engineer/server-generator-apollo-emmett 0.11.13 → 0.11.15
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/CHANGELOG.md +16 -0
- package/dist/src/codegen/extract/messages.d.ts +1 -0
- package/dist/src/codegen/extract/messages.d.ts.map +1 -1
- package/dist/src/codegen/extract/messages.js +4 -1
- package/dist/src/codegen/extract/messages.js.map +1 -1
- package/dist/src/codegen/extract/projection.d.ts +1 -0
- package/dist/src/codegen/extract/projection.d.ts.map +1 -1
- package/dist/src/codegen/extract/projection.js +12 -0
- package/dist/src/codegen/extract/projection.js.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +3 -2
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/query/projection.specs.specs.ts +366 -0
- package/dist/src/codegen/templates/query/projection.specs.ts +409 -1
- package/dist/src/codegen/templates/query/projection.specs.ts.ejs +6 -2
- package/dist/src/codegen/templates/query/projection.ts.ejs +69 -12
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/codegen/extract/messages.ts +6 -1
- package/src/codegen/extract/projection.ts +14 -0
- package/src/codegen/scaffoldFromSchema.ts +3 -0
- package/src/codegen/templates/query/projection.specs.specs.ts +366 -0
- package/src/codegen/templates/query/projection.specs.ts +409 -1
- package/src/codegen/templates/query/projection.specs.ts.ejs +6 -2
- package/src/codegen/templates/query/projection.ts.ejs +69 -12
package/package.json
CHANGED
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"graphql-type-json": "^0.3.2",
|
|
32
32
|
"uuid": "^11.0.0",
|
|
33
33
|
"web-streams-polyfill": "^4.1.0",
|
|
34
|
-
"@auto-engineer/narrative": "0.11.
|
|
35
|
-
"@auto-engineer/message-bus": "0.11.
|
|
34
|
+
"@auto-engineer/narrative": "0.11.15",
|
|
35
|
+
"@auto-engineer/message-bus": "0.11.15"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"typescript": "^5.8.3",
|
|
44
44
|
"vitest": "^3.2.4",
|
|
45
45
|
"tsx": "^4.19.2",
|
|
46
|
-
"@auto-engineer/cli": "0.11.
|
|
46
|
+
"@auto-engineer/cli": "0.11.15"
|
|
47
47
|
},
|
|
48
|
-
"version": "0.11.
|
|
48
|
+
"version": "0.11.15",
|
|
49
49
|
"scripts": {
|
|
50
50
|
"generate:server": "tsx src/cli/index.ts",
|
|
51
51
|
"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",
|
|
@@ -3,7 +3,7 @@ import { CommandExample, EventExample, Slice } from '@auto-engineer/narrative';
|
|
|
3
3
|
import { Message, MessageDefinition } from '../types';
|
|
4
4
|
import { extractEventsFromGiven, extractEventsFromThen, extractEventsFromWhen } from './events';
|
|
5
5
|
import { extractFieldsFromMessage } from './fields';
|
|
6
|
-
import { extractProjectionIdField } from './projection';
|
|
6
|
+
import { extractProjectionIdField, extractProjectionSingleton } from './projection';
|
|
7
7
|
import { extractStatesFromData, extractStatesFromTarget } from './states';
|
|
8
8
|
import createDebug from 'debug';
|
|
9
9
|
|
|
@@ -19,6 +19,7 @@ export interface ExtractedMessages {
|
|
|
19
19
|
states: Message[];
|
|
20
20
|
commandSchemasByName: Record<string, Message>;
|
|
21
21
|
projectionIdField?: string;
|
|
22
|
+
projectionSingleton?: boolean;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface ReactGwtSpec {
|
|
@@ -122,6 +123,9 @@ function extractMessagesForQuery(slice: Slice, allMessages: MessageDefinition[])
|
|
|
122
123
|
const projectionIdField = extractProjectionIdField(slice);
|
|
123
124
|
debugQuery(' Projection ID field: %s', projectionIdField ?? 'none');
|
|
124
125
|
|
|
126
|
+
const projectionSingleton = extractProjectionSingleton(slice);
|
|
127
|
+
debugQuery(' Projection singleton: %s', projectionSingleton);
|
|
128
|
+
|
|
125
129
|
const events: Message[] = gwtSpecs.flatMap((gwt) => {
|
|
126
130
|
const eventsFromGiven = Array.isArray(gwt.given)
|
|
127
131
|
? gwt.given.filter((item): item is EventExample => 'eventRef' in item)
|
|
@@ -166,6 +170,7 @@ function extractMessagesForQuery(slice: Slice, allMessages: MessageDefinition[])
|
|
|
166
170
|
states,
|
|
167
171
|
commandSchemasByName: {},
|
|
168
172
|
projectionIdField,
|
|
173
|
+
projectionSingleton,
|
|
169
174
|
};
|
|
170
175
|
|
|
171
176
|
debugQuery(' Final result: %d events, %d states', result.events.length, result.states.length);
|
|
@@ -4,6 +4,7 @@ interface ProjectionOrigin {
|
|
|
4
4
|
type: 'projection';
|
|
5
5
|
idField?: string;
|
|
6
6
|
name?: string;
|
|
7
|
+
singleton?: boolean;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
interface HasOrigin {
|
|
@@ -46,3 +47,16 @@ export function extractProjectionIdField(slice: Slice): string | undefined {
|
|
|
46
47
|
export function extractProjectionName(slice: Slice): string | undefined {
|
|
47
48
|
return extractProjectionField(slice, 'name');
|
|
48
49
|
}
|
|
50
|
+
|
|
51
|
+
export function extractProjectionSingleton(slice: Slice): boolean {
|
|
52
|
+
if (!('server' in slice)) return false;
|
|
53
|
+
const dataSource = slice.server?.data?.[0];
|
|
54
|
+
if (!hasOrigin(dataSource)) return false;
|
|
55
|
+
|
|
56
|
+
const origin = dataSource.origin;
|
|
57
|
+
if (isProjectionOrigin(origin)) {
|
|
58
|
+
return origin.singleton === true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
@@ -490,6 +490,7 @@ async function prepareTemplateData(
|
|
|
490
490
|
states: Message[],
|
|
491
491
|
commandSchemasByName: Record<string, Message>,
|
|
492
492
|
projectionIdField: string | undefined,
|
|
493
|
+
projectionSingleton: boolean | undefined,
|
|
493
494
|
allMessages?: MessageDefinition[],
|
|
494
495
|
integrations?: Model['integrations'],
|
|
495
496
|
): Promise<Record<string, unknown>> {
|
|
@@ -538,6 +539,7 @@ async function prepareTemplateData(
|
|
|
538
539
|
usedErrors,
|
|
539
540
|
commandSchemasByName,
|
|
540
541
|
projectionIdField,
|
|
542
|
+
projectionSingleton,
|
|
541
543
|
projectionName,
|
|
542
544
|
projectionType: projectionName != null ? pascalCase(projectionName) : undefined,
|
|
543
545
|
parsedRequest: slice.type === 'query' && slice.request != null ? parseGraphQlRequest(slice.request) : undefined,
|
|
@@ -693,6 +695,7 @@ async function generateFilesForSlice(
|
|
|
693
695
|
extracted.states,
|
|
694
696
|
extracted.commandSchemasByName,
|
|
695
697
|
extracted.projectionIdField,
|
|
698
|
+
extracted.projectionSingleton,
|
|
696
699
|
messages,
|
|
697
700
|
integrations,
|
|
698
701
|
);
|
|
@@ -620,4 +620,370 @@ describe('projection.specs.ts.ejs', () => {
|
|
|
620
620
|
// canHandle must include BOTH events
|
|
621
621
|
expect(projectionFile?.contents).toContain("canHandle: ['QuestionnaireLinkSent', 'QuestionAnswered']");
|
|
622
622
|
});
|
|
623
|
+
|
|
624
|
+
it('should generate a valid test spec for singleton projection', async () => {
|
|
625
|
+
const spec: SpecsSchema = {
|
|
626
|
+
variant: 'specs',
|
|
627
|
+
narratives: [
|
|
628
|
+
{
|
|
629
|
+
name: 'todo-flow',
|
|
630
|
+
slices: [
|
|
631
|
+
{
|
|
632
|
+
type: 'command',
|
|
633
|
+
name: 'manage-todo',
|
|
634
|
+
stream: 'todo-${todoId}',
|
|
635
|
+
client: { description: '' },
|
|
636
|
+
server: {
|
|
637
|
+
description: '',
|
|
638
|
+
specs: {
|
|
639
|
+
name: 'Manage todo command',
|
|
640
|
+
rules: [
|
|
641
|
+
{
|
|
642
|
+
description: 'Should handle todo operations',
|
|
643
|
+
examples: [
|
|
644
|
+
{
|
|
645
|
+
description: 'User adds todo',
|
|
646
|
+
when: {
|
|
647
|
+
commandRef: 'AddTodo',
|
|
648
|
+
exampleData: {
|
|
649
|
+
todoId: 'todo_123',
|
|
650
|
+
title: 'Buy milk',
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
then: [
|
|
654
|
+
{
|
|
655
|
+
eventRef: 'TodoAdded',
|
|
656
|
+
exampleData: {
|
|
657
|
+
todoId: 'todo_123',
|
|
658
|
+
title: 'Buy milk',
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
},
|
|
663
|
+
],
|
|
664
|
+
},
|
|
665
|
+
],
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
type: 'query',
|
|
671
|
+
name: 'view-summary',
|
|
672
|
+
stream: 'todos',
|
|
673
|
+
client: { description: '' },
|
|
674
|
+
server: {
|
|
675
|
+
description: '',
|
|
676
|
+
data: [
|
|
677
|
+
{
|
|
678
|
+
target: {
|
|
679
|
+
type: 'State',
|
|
680
|
+
name: 'TodoSummary',
|
|
681
|
+
},
|
|
682
|
+
origin: {
|
|
683
|
+
type: 'projection',
|
|
684
|
+
name: 'TodoSummaryProjection',
|
|
685
|
+
singleton: true,
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
],
|
|
689
|
+
specs: {
|
|
690
|
+
name: 'View summary query',
|
|
691
|
+
rules: [
|
|
692
|
+
{
|
|
693
|
+
description: 'Should aggregate todo counts',
|
|
694
|
+
examples: [
|
|
695
|
+
{
|
|
696
|
+
description: 'Todo added updates count',
|
|
697
|
+
when: [
|
|
698
|
+
{
|
|
699
|
+
eventRef: 'TodoAdded',
|
|
700
|
+
exampleData: {
|
|
701
|
+
todoId: 'todo_123',
|
|
702
|
+
title: 'Buy milk',
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
],
|
|
706
|
+
then: [
|
|
707
|
+
{
|
|
708
|
+
stateRef: 'TodoSummary',
|
|
709
|
+
exampleData: {
|
|
710
|
+
totalCount: 1,
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
],
|
|
714
|
+
},
|
|
715
|
+
],
|
|
716
|
+
},
|
|
717
|
+
],
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
],
|
|
722
|
+
},
|
|
723
|
+
],
|
|
724
|
+
messages: [
|
|
725
|
+
{
|
|
726
|
+
type: 'command',
|
|
727
|
+
name: 'AddTodo',
|
|
728
|
+
fields: [
|
|
729
|
+
{ name: 'todoId', type: 'string', required: true },
|
|
730
|
+
{ name: 'title', type: 'string', required: true },
|
|
731
|
+
],
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
type: 'event',
|
|
735
|
+
name: 'TodoAdded',
|
|
736
|
+
source: 'internal',
|
|
737
|
+
fields: [
|
|
738
|
+
{ name: 'todoId', type: 'string', required: true },
|
|
739
|
+
{ name: 'title', type: 'string', required: true },
|
|
740
|
+
],
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
type: 'state',
|
|
744
|
+
name: 'TodoSummary',
|
|
745
|
+
fields: [{ name: 'totalCount', type: 'number', required: true }],
|
|
746
|
+
},
|
|
747
|
+
],
|
|
748
|
+
} as SpecsSchema;
|
|
749
|
+
|
|
750
|
+
const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
751
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('view-summary/projection.specs.ts'));
|
|
752
|
+
|
|
753
|
+
expect(specFile?.contents).toMatchInlineSnapshot(`
|
|
754
|
+
"import { describe, it, beforeEach, expect } from 'vitest';
|
|
755
|
+
import { InMemoryProjectionSpec } from '@event-driven-io/emmett';
|
|
756
|
+
import { projection } from './projection';
|
|
757
|
+
import type { TodoAdded } from '../manage-todo/events';
|
|
758
|
+
import { TodoSummary } from './state';
|
|
759
|
+
|
|
760
|
+
type ProjectionEvent = TodoAdded;
|
|
761
|
+
|
|
762
|
+
describe('Should aggregate todo counts', () => {
|
|
763
|
+
let given: InMemoryProjectionSpec<ProjectionEvent>;
|
|
764
|
+
|
|
765
|
+
beforeEach(() => {
|
|
766
|
+
given = InMemoryProjectionSpec.for({ projection });
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it('Todo added updates count', () =>
|
|
770
|
+
given([])
|
|
771
|
+
.when([
|
|
772
|
+
{
|
|
773
|
+
type: 'TodoAdded',
|
|
774
|
+
data: {
|
|
775
|
+
todoId: 'todo_123',
|
|
776
|
+
title: 'Buy milk',
|
|
777
|
+
},
|
|
778
|
+
metadata: {
|
|
779
|
+
streamName: 'todos',
|
|
780
|
+
streamPosition: 1n,
|
|
781
|
+
globalPosition: 1n,
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
])
|
|
785
|
+
.then(async (state) => {
|
|
786
|
+
const document = await state.database.collection<TodoSummary>('TodoSummaryProjection').findOne();
|
|
787
|
+
|
|
788
|
+
const expected: TodoSummary = {
|
|
789
|
+
totalCount: 1,
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
expect(document).toMatchObject(expected);
|
|
793
|
+
}));
|
|
794
|
+
});
|
|
795
|
+
"
|
|
796
|
+
`);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it('should generate a valid test spec for composite key projection', async () => {
|
|
800
|
+
const spec: SpecsSchema = {
|
|
801
|
+
variant: 'specs',
|
|
802
|
+
narratives: [
|
|
803
|
+
{
|
|
804
|
+
name: 'user-project-flow',
|
|
805
|
+
slices: [
|
|
806
|
+
{
|
|
807
|
+
type: 'command',
|
|
808
|
+
name: 'manage-user-project',
|
|
809
|
+
stream: 'user-project-${userId}-${projectId}',
|
|
810
|
+
client: { description: '' },
|
|
811
|
+
server: {
|
|
812
|
+
description: '',
|
|
813
|
+
specs: {
|
|
814
|
+
name: 'Manage user project command',
|
|
815
|
+
rules: [
|
|
816
|
+
{
|
|
817
|
+
description: 'Should handle user project operations',
|
|
818
|
+
examples: [
|
|
819
|
+
{
|
|
820
|
+
description: 'User joins project',
|
|
821
|
+
when: {
|
|
822
|
+
commandRef: 'JoinProject',
|
|
823
|
+
exampleData: {
|
|
824
|
+
userId: 'user_123',
|
|
825
|
+
projectId: 'proj_456',
|
|
826
|
+
role: 'developer',
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
then: [
|
|
830
|
+
{
|
|
831
|
+
eventRef: 'UserJoinedProject',
|
|
832
|
+
exampleData: {
|
|
833
|
+
userId: 'user_123',
|
|
834
|
+
projectId: 'proj_456',
|
|
835
|
+
role: 'developer',
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
],
|
|
839
|
+
},
|
|
840
|
+
],
|
|
841
|
+
},
|
|
842
|
+
],
|
|
843
|
+
},
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
type: 'query',
|
|
848
|
+
name: 'view-user-projects',
|
|
849
|
+
stream: 'user-projects',
|
|
850
|
+
client: { description: '' },
|
|
851
|
+
server: {
|
|
852
|
+
description: '',
|
|
853
|
+
data: [
|
|
854
|
+
{
|
|
855
|
+
target: {
|
|
856
|
+
type: 'State',
|
|
857
|
+
name: 'UserProject',
|
|
858
|
+
},
|
|
859
|
+
origin: {
|
|
860
|
+
type: 'projection',
|
|
861
|
+
name: 'UserProjectsProjection',
|
|
862
|
+
idField: ['userId', 'projectId'],
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
],
|
|
866
|
+
specs: {
|
|
867
|
+
name: 'View user projects query',
|
|
868
|
+
rules: [
|
|
869
|
+
{
|
|
870
|
+
description: 'Should track user project memberships',
|
|
871
|
+
examples: [
|
|
872
|
+
{
|
|
873
|
+
description: 'User joins project',
|
|
874
|
+
when: [
|
|
875
|
+
{
|
|
876
|
+
eventRef: 'UserJoinedProject',
|
|
877
|
+
exampleData: {
|
|
878
|
+
userId: 'user_123',
|
|
879
|
+
projectId: 'proj_456',
|
|
880
|
+
role: 'developer',
|
|
881
|
+
},
|
|
882
|
+
},
|
|
883
|
+
],
|
|
884
|
+
then: [
|
|
885
|
+
{
|
|
886
|
+
stateRef: 'UserProject',
|
|
887
|
+
exampleData: {
|
|
888
|
+
userId: 'user_123',
|
|
889
|
+
projectId: 'proj_456',
|
|
890
|
+
role: 'developer',
|
|
891
|
+
},
|
|
892
|
+
},
|
|
893
|
+
],
|
|
894
|
+
},
|
|
895
|
+
],
|
|
896
|
+
},
|
|
897
|
+
],
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
},
|
|
901
|
+
],
|
|
902
|
+
},
|
|
903
|
+
],
|
|
904
|
+
messages: [
|
|
905
|
+
{
|
|
906
|
+
type: 'command',
|
|
907
|
+
name: 'JoinProject',
|
|
908
|
+
fields: [
|
|
909
|
+
{ name: 'userId', type: 'string', required: true },
|
|
910
|
+
{ name: 'projectId', type: 'string', required: true },
|
|
911
|
+
{ name: 'role', type: 'string', required: true },
|
|
912
|
+
],
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
type: 'event',
|
|
916
|
+
name: 'UserJoinedProject',
|
|
917
|
+
source: 'internal',
|
|
918
|
+
fields: [
|
|
919
|
+
{ name: 'userId', type: 'string', required: true },
|
|
920
|
+
{ name: 'projectId', type: 'string', required: true },
|
|
921
|
+
{ name: 'role', type: 'string', required: true },
|
|
922
|
+
],
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
type: 'state',
|
|
926
|
+
name: 'UserProject',
|
|
927
|
+
fields: [
|
|
928
|
+
{ name: 'userId', type: 'string', required: true },
|
|
929
|
+
{ name: 'projectId', type: 'string', required: true },
|
|
930
|
+
{ name: 'role', type: 'string', required: true },
|
|
931
|
+
],
|
|
932
|
+
},
|
|
933
|
+
],
|
|
934
|
+
} as SpecsSchema;
|
|
935
|
+
|
|
936
|
+
const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
937
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('view-user-projects/projection.specs.ts'));
|
|
938
|
+
|
|
939
|
+
expect(specFile?.contents).toMatchInlineSnapshot(`
|
|
940
|
+
"import { describe, it, beforeEach, expect } from 'vitest';
|
|
941
|
+
import { InMemoryProjectionSpec } from '@event-driven-io/emmett';
|
|
942
|
+
import { projection } from './projection';
|
|
943
|
+
import type { UserJoinedProject } from '../manage-user-project/events';
|
|
944
|
+
import { UserProject } from './state';
|
|
945
|
+
|
|
946
|
+
type ProjectionEvent = UserJoinedProject;
|
|
947
|
+
|
|
948
|
+
describe('Should track user project memberships', () => {
|
|
949
|
+
let given: InMemoryProjectionSpec<ProjectionEvent>;
|
|
950
|
+
|
|
951
|
+
beforeEach(() => {
|
|
952
|
+
given = InMemoryProjectionSpec.for({ projection });
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
it('User joins project', () =>
|
|
956
|
+
given([])
|
|
957
|
+
.when([
|
|
958
|
+
{
|
|
959
|
+
type: 'UserJoinedProject',
|
|
960
|
+
data: {
|
|
961
|
+
userId: 'user_123',
|
|
962
|
+
projectId: 'proj_456',
|
|
963
|
+
role: 'developer',
|
|
964
|
+
},
|
|
965
|
+
metadata: {
|
|
966
|
+
streamName: 'user-projects',
|
|
967
|
+
streamPosition: 1n,
|
|
968
|
+
globalPosition: 1n,
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
])
|
|
972
|
+
.then(async (state) => {
|
|
973
|
+
const document = await state.database
|
|
974
|
+
.collection<UserProject>('UserProjectsProjection')
|
|
975
|
+
.findOne((doc) => doc.id === 'test-id');
|
|
976
|
+
|
|
977
|
+
const expected: UserProject = {
|
|
978
|
+
userId: 'user_123',
|
|
979
|
+
projectId: 'proj_456',
|
|
980
|
+
role: 'developer',
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
expect(document).toMatchObject(expected);
|
|
984
|
+
}));
|
|
985
|
+
});
|
|
986
|
+
"
|
|
987
|
+
`);
|
|
988
|
+
});
|
|
623
989
|
});
|