@workos/oagen-emitters 0.2.0 → 0.2.1
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/.oxfmtrc.json +8 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +633 -85
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/smoke/sdk-dotnet.ts +17 -3
- package/smoke/sdk-elixir.ts +17 -3
- package/smoke/sdk-go.ts +21 -4
- package/smoke/sdk-kotlin.ts +23 -4
- package/smoke/sdk-node.ts +15 -3
- package/smoke/sdk-ruby.ts +17 -3
- package/smoke/sdk-rust.ts +16 -3
- package/src/node/client.ts +94 -12
- package/src/node/common.ts +1 -1
- package/src/node/enums.ts +4 -4
- package/src/node/errors.ts +5 -1
- package/src/node/fixtures.ts +6 -4
- package/src/node/index.ts +65 -9
- package/src/node/models.ts +86 -75
- package/src/node/naming.ts +91 -2
- package/src/node/resources.ts +462 -23
- package/src/node/serializers.ts +3 -1
- package/src/node/tests.ts +39 -15
- package/src/node/utils.ts +52 -2
- package/test/node/client.test.ts +181 -82
- package/test/node/enums.test.ts +73 -3
- package/test/node/models.test.ts +107 -20
- package/test/node/naming.test.ts +14 -4
- package/test/node/resources.test.ts +627 -25
- package/test/node/serializers.test.ts +33 -6
|
@@ -72,7 +72,10 @@ describe('generateResources', () => {
|
|
|
72
72
|
queryParams: [
|
|
73
73
|
{
|
|
74
74
|
name: 'domains',
|
|
75
|
-
type: {
|
|
75
|
+
type: {
|
|
76
|
+
kind: 'array',
|
|
77
|
+
items: { kind: 'primitive', type: 'string' },
|
|
78
|
+
},
|
|
76
79
|
required: false,
|
|
77
80
|
},
|
|
78
81
|
],
|
|
@@ -243,7 +246,12 @@ describe('generateResources', () => {
|
|
|
243
246
|
methodByOperation: new Map([
|
|
244
247
|
[
|
|
245
248
|
'POST /auth/factors/enroll',
|
|
246
|
-
{
|
|
249
|
+
{
|
|
250
|
+
className: 'Mfa',
|
|
251
|
+
methodName: 'enrollFactor',
|
|
252
|
+
params: [],
|
|
253
|
+
returnType: 'void',
|
|
254
|
+
},
|
|
247
255
|
],
|
|
248
256
|
]),
|
|
249
257
|
httpKeyByMethod: new Map(),
|
|
@@ -315,7 +323,13 @@ describe('generateResources', () => {
|
|
|
315
323
|
name: 'getOrganization',
|
|
316
324
|
httpMethod: 'get',
|
|
317
325
|
path: '/organizations/{id}',
|
|
318
|
-
pathParams: [
|
|
326
|
+
pathParams: [
|
|
327
|
+
{
|
|
328
|
+
name: 'id',
|
|
329
|
+
type: { kind: 'primitive', type: 'string' },
|
|
330
|
+
required: true,
|
|
331
|
+
},
|
|
332
|
+
],
|
|
319
333
|
queryParams: [],
|
|
320
334
|
headerParams: [],
|
|
321
335
|
response: { kind: 'model', name: 'Organization' },
|
|
@@ -340,7 +354,13 @@ describe('generateResources', () => {
|
|
|
340
354
|
name: 'getOrganization',
|
|
341
355
|
httpMethod: 'get',
|
|
342
356
|
path: '/organizations/{id}',
|
|
343
|
-
pathParams: [
|
|
357
|
+
pathParams: [
|
|
358
|
+
{
|
|
359
|
+
name: 'id',
|
|
360
|
+
type: { kind: 'primitive', type: 'string' },
|
|
361
|
+
required: true,
|
|
362
|
+
},
|
|
363
|
+
],
|
|
344
364
|
queryParams: [
|
|
345
365
|
{
|
|
346
366
|
name: 'include_fields',
|
|
@@ -372,7 +392,13 @@ describe('generateResources', () => {
|
|
|
372
392
|
name: 'getSession',
|
|
373
393
|
httpMethod: 'get',
|
|
374
394
|
path: '/sessions/{id}',
|
|
375
|
-
pathParams: [
|
|
395
|
+
pathParams: [
|
|
396
|
+
{
|
|
397
|
+
name: 'id',
|
|
398
|
+
type: { kind: 'primitive', type: 'string' },
|
|
399
|
+
required: true,
|
|
400
|
+
},
|
|
401
|
+
],
|
|
376
402
|
queryParams: [],
|
|
377
403
|
headerParams: [
|
|
378
404
|
{
|
|
@@ -421,8 +447,14 @@ describe('generateResources', () => {
|
|
|
421
447
|
requestBody: { kind: 'model', name: 'CreateOrganizationInput' },
|
|
422
448
|
response: { kind: 'model', name: 'Organization' },
|
|
423
449
|
successResponses: [
|
|
424
|
-
{
|
|
425
|
-
|
|
450
|
+
{
|
|
451
|
+
statusCode: 200,
|
|
452
|
+
type: { kind: 'model', name: 'Organization' },
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
statusCode: 201,
|
|
456
|
+
type: { kind: 'model', name: 'Organization' },
|
|
457
|
+
},
|
|
426
458
|
],
|
|
427
459
|
errors: [],
|
|
428
460
|
injectIdempotencyKey: false,
|
|
@@ -517,7 +549,13 @@ describe('generateResources', () => {
|
|
|
517
549
|
name: 'getOrganization',
|
|
518
550
|
httpMethod: 'get',
|
|
519
551
|
path: '/organizations/{id}',
|
|
520
|
-
pathParams: [
|
|
552
|
+
pathParams: [
|
|
553
|
+
{
|
|
554
|
+
name: 'id',
|
|
555
|
+
type: { kind: 'primitive', type: 'string' },
|
|
556
|
+
required: true,
|
|
557
|
+
},
|
|
558
|
+
],
|
|
521
559
|
queryParams: [
|
|
522
560
|
{
|
|
523
561
|
name: 'include_fields',
|
|
@@ -603,7 +641,7 @@ describe('generateResources', () => {
|
|
|
603
641
|
expect(content).toContain('query: options');
|
|
604
642
|
});
|
|
605
643
|
|
|
606
|
-
it('
|
|
644
|
+
it('falls back to pass-through for non-discriminated union when models not in spec', () => {
|
|
607
645
|
const services: Service[] = [
|
|
608
646
|
{
|
|
609
647
|
name: 'Auth',
|
|
@@ -640,7 +678,7 @@ describe('generateResources', () => {
|
|
|
640
678
|
// Should NOT use Record<string, unknown>
|
|
641
679
|
expect(content).not.toContain('Record<string, unknown>');
|
|
642
680
|
|
|
643
|
-
//
|
|
681
|
+
// Models not in spec → falls back to pass-through
|
|
644
682
|
expect(content).toContain("'/user_management/authenticate',");
|
|
645
683
|
expect(content).toContain('payload,');
|
|
646
684
|
|
|
@@ -650,6 +688,111 @@ describe('generateResources', () => {
|
|
|
650
688
|
expect(content).toContain('AuthByMagicAuth');
|
|
651
689
|
});
|
|
652
690
|
|
|
691
|
+
it('generates field-guard serializer dispatch for non-discriminated union with models', () => {
|
|
692
|
+
const services: Service[] = [
|
|
693
|
+
{
|
|
694
|
+
name: 'Applications',
|
|
695
|
+
operations: [
|
|
696
|
+
{
|
|
697
|
+
name: 'createApplication',
|
|
698
|
+
httpMethod: 'post',
|
|
699
|
+
path: '/connect/applications',
|
|
700
|
+
pathParams: [],
|
|
701
|
+
queryParams: [],
|
|
702
|
+
headerParams: [],
|
|
703
|
+
requestBody: {
|
|
704
|
+
kind: 'union',
|
|
705
|
+
variants: [
|
|
706
|
+
{ kind: 'model', name: 'CreateOAuthApplication' },
|
|
707
|
+
{ kind: 'model', name: 'CreateM2MApplication' },
|
|
708
|
+
],
|
|
709
|
+
},
|
|
710
|
+
response: { kind: 'model', name: 'ConnectApplication' },
|
|
711
|
+
errors: [],
|
|
712
|
+
injectIdempotencyKey: false,
|
|
713
|
+
},
|
|
714
|
+
],
|
|
715
|
+
},
|
|
716
|
+
];
|
|
717
|
+
|
|
718
|
+
const testCtx: EmitterContext = {
|
|
719
|
+
namespace: 'workos',
|
|
720
|
+
namespacePascal: 'WorkOS',
|
|
721
|
+
spec: {
|
|
722
|
+
...emptySpec,
|
|
723
|
+
services,
|
|
724
|
+
models: [
|
|
725
|
+
{
|
|
726
|
+
name: 'CreateOAuthApplication',
|
|
727
|
+
fields: [
|
|
728
|
+
{
|
|
729
|
+
name: 'name',
|
|
730
|
+
type: { kind: 'primitive', type: 'string' },
|
|
731
|
+
required: true,
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
name: 'redirect_uris',
|
|
735
|
+
type: {
|
|
736
|
+
kind: 'array',
|
|
737
|
+
items: { kind: 'primitive', type: 'string' },
|
|
738
|
+
},
|
|
739
|
+
required: true,
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
name: 'uses_pkce',
|
|
743
|
+
type: { kind: 'primitive', type: 'boolean' },
|
|
744
|
+
required: false,
|
|
745
|
+
},
|
|
746
|
+
],
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
name: 'CreateM2MApplication',
|
|
750
|
+
fields: [
|
|
751
|
+
{
|
|
752
|
+
name: 'name',
|
|
753
|
+
type: { kind: 'primitive', type: 'string' },
|
|
754
|
+
required: true,
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
name: 'scopes',
|
|
758
|
+
type: {
|
|
759
|
+
kind: 'array',
|
|
760
|
+
items: { kind: 'primitive', type: 'string' },
|
|
761
|
+
},
|
|
762
|
+
required: true,
|
|
763
|
+
},
|
|
764
|
+
],
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
name: 'ConnectApplication',
|
|
768
|
+
fields: [
|
|
769
|
+
{
|
|
770
|
+
name: 'id',
|
|
771
|
+
type: { kind: 'primitive', type: 'string' },
|
|
772
|
+
required: true,
|
|
773
|
+
},
|
|
774
|
+
],
|
|
775
|
+
},
|
|
776
|
+
],
|
|
777
|
+
},
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
const files = generateResources(services, testCtx);
|
|
781
|
+
const content = files[0].content;
|
|
782
|
+
|
|
783
|
+
// Should use the union type for the payload parameter
|
|
784
|
+
expect(content).toContain('payload: CreateOAuthApplication | CreateM2MApplication');
|
|
785
|
+
|
|
786
|
+
// Should dispatch via unique required field guards
|
|
787
|
+
expect(content).toContain("'redirectUris' in payload");
|
|
788
|
+
expect(content).toContain('serializeCreateOAuthApplication(payload as any)');
|
|
789
|
+
expect(content).toContain('serializeCreateM2MApplication(payload as any)');
|
|
790
|
+
|
|
791
|
+
// Should import serializers for all union variants
|
|
792
|
+
expect(content).toContain('serializeCreateOAuthApplication');
|
|
793
|
+
expect(content).toContain('serializeCreateM2MApplication');
|
|
794
|
+
});
|
|
795
|
+
|
|
653
796
|
it('generates discriminated union serializer dispatch for request body', () => {
|
|
654
797
|
const services: Service[] = [
|
|
655
798
|
{
|
|
@@ -790,12 +933,12 @@ describe('generateResources', () => {
|
|
|
790
933
|
it('prefixes ListOptions with service name when method is "list"', () => {
|
|
791
934
|
const services: Service[] = [
|
|
792
935
|
{
|
|
793
|
-
name: '
|
|
936
|
+
name: 'Payments',
|
|
794
937
|
operations: [
|
|
795
938
|
{
|
|
796
939
|
name: 'list',
|
|
797
940
|
httpMethod: 'get',
|
|
798
|
-
path: '/
|
|
941
|
+
path: '/payments',
|
|
799
942
|
pathParams: [],
|
|
800
943
|
queryParams: [
|
|
801
944
|
{
|
|
@@ -826,7 +969,15 @@ describe('generateResources', () => {
|
|
|
826
969
|
spec: { ...emptySpec, services, models: [] },
|
|
827
970
|
overlayLookup: {
|
|
828
971
|
methodByOperation: new Map([
|
|
829
|
-
[
|
|
972
|
+
[
|
|
973
|
+
'GET /payments',
|
|
974
|
+
{
|
|
975
|
+
className: 'Payments',
|
|
976
|
+
methodName: 'list',
|
|
977
|
+
params: [],
|
|
978
|
+
returnType: 'void',
|
|
979
|
+
},
|
|
980
|
+
],
|
|
830
981
|
]),
|
|
831
982
|
httpKeyByMethod: new Map(),
|
|
832
983
|
interfaceByName: new Map(),
|
|
@@ -841,8 +992,8 @@ describe('generateResources', () => {
|
|
|
841
992
|
const content = files[0].content;
|
|
842
993
|
|
|
843
994
|
// Should use service-prefixed options name instead of generic "ListOptions"
|
|
844
|
-
expect(content).toContain('export interface
|
|
845
|
-
expect(content).toContain('Promise<AutoPaginatable<Connection,
|
|
995
|
+
expect(content).toContain('export interface PaymentsListOptions extends PaginationOptions {');
|
|
996
|
+
expect(content).toContain('Promise<AutoPaginatable<Connection, PaymentsListOptions>>');
|
|
846
997
|
// Should NOT use the generic "ListOptions"
|
|
847
998
|
expect(content).not.toContain('export interface ListOptions ');
|
|
848
999
|
});
|
|
@@ -860,7 +1011,10 @@ describe('generateResources', () => {
|
|
|
860
1011
|
queryParams: [
|
|
861
1012
|
{
|
|
862
1013
|
name: 'domains',
|
|
863
|
-
type: {
|
|
1014
|
+
type: {
|
|
1015
|
+
kind: 'array',
|
|
1016
|
+
items: { kind: 'primitive', type: 'string' },
|
|
1017
|
+
},
|
|
864
1018
|
required: false,
|
|
865
1019
|
},
|
|
866
1020
|
],
|
|
@@ -885,6 +1039,185 @@ describe('generateResources', () => {
|
|
|
885
1039
|
// Method is "listOrganizations", not "list", so options name should be normal
|
|
886
1040
|
expect(content).toContain('export interface ListOrganizationsOptions extends PaginationOptions {');
|
|
887
1041
|
});
|
|
1042
|
+
|
|
1043
|
+
it('removes skipIfExists when fully-covered service has methods absent from baseline', () => {
|
|
1044
|
+
const services: Service[] = [
|
|
1045
|
+
{
|
|
1046
|
+
name: 'SSOService',
|
|
1047
|
+
operations: [
|
|
1048
|
+
{
|
|
1049
|
+
name: 'getAuthorizationUrl',
|
|
1050
|
+
httpMethod: 'get',
|
|
1051
|
+
path: '/sso/authorize',
|
|
1052
|
+
pathParams: [],
|
|
1053
|
+
queryParams: [],
|
|
1054
|
+
headerParams: [],
|
|
1055
|
+
response: { kind: 'model', name: 'AuthorizationUrl' },
|
|
1056
|
+
errors: [],
|
|
1057
|
+
injectIdempotencyKey: false,
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
name: 'logout',
|
|
1061
|
+
httpMethod: 'get',
|
|
1062
|
+
path: '/sso/logout',
|
|
1063
|
+
pathParams: [],
|
|
1064
|
+
queryParams: [],
|
|
1065
|
+
headerParams: [],
|
|
1066
|
+
response: { kind: 'model', name: 'LogoutResult' },
|
|
1067
|
+
errors: [],
|
|
1068
|
+
injectIdempotencyKey: false,
|
|
1069
|
+
},
|
|
1070
|
+
],
|
|
1071
|
+
},
|
|
1072
|
+
];
|
|
1073
|
+
|
|
1074
|
+
// Overlay maps both operations to SSO class
|
|
1075
|
+
// Baseline SSO class exists but only has getAuthorizationUrl (logout is missing)
|
|
1076
|
+
const overlayCtx: EmitterContext = {
|
|
1077
|
+
namespace: 'workos',
|
|
1078
|
+
namespacePascal: 'WorkOS',
|
|
1079
|
+
spec: { ...emptySpec, services, models: [] },
|
|
1080
|
+
overlayLookup: {
|
|
1081
|
+
methodByOperation: new Map([
|
|
1082
|
+
[
|
|
1083
|
+
'GET /sso/authorize',
|
|
1084
|
+
{
|
|
1085
|
+
className: 'SSO',
|
|
1086
|
+
methodName: 'getAuthorizationUrl',
|
|
1087
|
+
params: [],
|
|
1088
|
+
returnType: 'void',
|
|
1089
|
+
},
|
|
1090
|
+
],
|
|
1091
|
+
[
|
|
1092
|
+
'GET /sso/logout',
|
|
1093
|
+
{
|
|
1094
|
+
className: 'SSO',
|
|
1095
|
+
methodName: 'logout',
|
|
1096
|
+
params: [],
|
|
1097
|
+
returnType: 'void',
|
|
1098
|
+
},
|
|
1099
|
+
],
|
|
1100
|
+
]),
|
|
1101
|
+
httpKeyByMethod: new Map(),
|
|
1102
|
+
interfaceByName: new Map(),
|
|
1103
|
+
typeAliasByName: new Map(),
|
|
1104
|
+
requiredExports: new Map(),
|
|
1105
|
+
modelNameByIR: new Map(),
|
|
1106
|
+
fileBySymbol: new Map(),
|
|
1107
|
+
},
|
|
1108
|
+
apiSurface: {
|
|
1109
|
+
language: 'node',
|
|
1110
|
+
extractedFrom: 'test',
|
|
1111
|
+
extractedAt: '2024-01-01',
|
|
1112
|
+
classes: {
|
|
1113
|
+
SSO: {
|
|
1114
|
+
name: 'SSO',
|
|
1115
|
+
methods: {
|
|
1116
|
+
getAuthorizationUrl: [
|
|
1117
|
+
{
|
|
1118
|
+
name: 'getAuthorizationUrl',
|
|
1119
|
+
params: [],
|
|
1120
|
+
returnType: 'void',
|
|
1121
|
+
async: true,
|
|
1122
|
+
},
|
|
1123
|
+
],
|
|
1124
|
+
// logout method is intentionally ABSENT
|
|
1125
|
+
},
|
|
1126
|
+
properties: {},
|
|
1127
|
+
constructorParams: [{ name: 'workos', type: 'WorkOS', optional: false }],
|
|
1128
|
+
},
|
|
1129
|
+
},
|
|
1130
|
+
interfaces: {},
|
|
1131
|
+
typeAliases: {},
|
|
1132
|
+
enums: {},
|
|
1133
|
+
exports: {},
|
|
1134
|
+
},
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
const files = generateResources(services, overlayCtx);
|
|
1138
|
+
expect(files.length).toBe(1);
|
|
1139
|
+
|
|
1140
|
+
// skipIfExists should be removed because 'logout' is absent from baseline
|
|
1141
|
+
expect(files[0].skipIfExists).toBeUndefined();
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
it('keeps skipIfExists when fully-covered service has all methods in baseline', () => {
|
|
1145
|
+
const services: Service[] = [
|
|
1146
|
+
{
|
|
1147
|
+
name: 'SSOService',
|
|
1148
|
+
operations: [
|
|
1149
|
+
{
|
|
1150
|
+
name: 'getAuthorizationUrl',
|
|
1151
|
+
httpMethod: 'get',
|
|
1152
|
+
path: '/sso/authorize',
|
|
1153
|
+
pathParams: [],
|
|
1154
|
+
queryParams: [],
|
|
1155
|
+
headerParams: [],
|
|
1156
|
+
response: { kind: 'model', name: 'AuthorizationUrl' },
|
|
1157
|
+
errors: [],
|
|
1158
|
+
injectIdempotencyKey: false,
|
|
1159
|
+
},
|
|
1160
|
+
],
|
|
1161
|
+
},
|
|
1162
|
+
];
|
|
1163
|
+
|
|
1164
|
+
const overlayCtx: EmitterContext = {
|
|
1165
|
+
namespace: 'workos',
|
|
1166
|
+
namespacePascal: 'WorkOS',
|
|
1167
|
+
spec: { ...emptySpec, services, models: [] },
|
|
1168
|
+
overlayLookup: {
|
|
1169
|
+
methodByOperation: new Map([
|
|
1170
|
+
[
|
|
1171
|
+
'GET /sso/authorize',
|
|
1172
|
+
{
|
|
1173
|
+
className: 'SSO',
|
|
1174
|
+
methodName: 'getAuthorizationUrl',
|
|
1175
|
+
params: [],
|
|
1176
|
+
returnType: 'void',
|
|
1177
|
+
},
|
|
1178
|
+
],
|
|
1179
|
+
]),
|
|
1180
|
+
httpKeyByMethod: new Map(),
|
|
1181
|
+
interfaceByName: new Map(),
|
|
1182
|
+
typeAliasByName: new Map(),
|
|
1183
|
+
requiredExports: new Map(),
|
|
1184
|
+
modelNameByIR: new Map(),
|
|
1185
|
+
fileBySymbol: new Map(),
|
|
1186
|
+
},
|
|
1187
|
+
apiSurface: {
|
|
1188
|
+
language: 'node',
|
|
1189
|
+
extractedFrom: 'test',
|
|
1190
|
+
extractedAt: '2024-01-01',
|
|
1191
|
+
classes: {
|
|
1192
|
+
SSO: {
|
|
1193
|
+
name: 'SSO',
|
|
1194
|
+
methods: {
|
|
1195
|
+
getAuthorizationUrl: [
|
|
1196
|
+
{
|
|
1197
|
+
name: 'getAuthorizationUrl',
|
|
1198
|
+
params: [],
|
|
1199
|
+
returnType: 'void',
|
|
1200
|
+
async: true,
|
|
1201
|
+
},
|
|
1202
|
+
],
|
|
1203
|
+
},
|
|
1204
|
+
properties: {},
|
|
1205
|
+
constructorParams: [{ name: 'workos', type: 'WorkOS', optional: false }],
|
|
1206
|
+
},
|
|
1207
|
+
},
|
|
1208
|
+
interfaces: {},
|
|
1209
|
+
typeAliases: {},
|
|
1210
|
+
enums: {},
|
|
1211
|
+
exports: {},
|
|
1212
|
+
},
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
const files = generateResources(services, overlayCtx);
|
|
1216
|
+
expect(files.length).toBe(1);
|
|
1217
|
+
|
|
1218
|
+
// skipIfExists should stay true because all methods exist in baseline
|
|
1219
|
+
expect(files[0].skipIfExists).toBe(true);
|
|
1220
|
+
});
|
|
888
1221
|
});
|
|
889
1222
|
|
|
890
1223
|
describe('resolveResourceClassName', () => {
|
|
@@ -914,7 +1247,12 @@ describe('resolveResourceClassName', () => {
|
|
|
914
1247
|
methodByOperation: new Map([
|
|
915
1248
|
[
|
|
916
1249
|
'GET /webhook_events',
|
|
917
|
-
{
|
|
1250
|
+
{
|
|
1251
|
+
className: 'Webhooks',
|
|
1252
|
+
methodName: 'listWebhookEvents',
|
|
1253
|
+
params: [],
|
|
1254
|
+
returnType: 'void',
|
|
1255
|
+
},
|
|
918
1256
|
],
|
|
919
1257
|
]),
|
|
920
1258
|
httpKeyByMethod: new Map(),
|
|
@@ -933,7 +1271,13 @@ describe('resolveResourceClassName', () => {
|
|
|
933
1271
|
name: 'Webhooks',
|
|
934
1272
|
methods: {},
|
|
935
1273
|
properties: {},
|
|
936
|
-
constructorParams: [
|
|
1274
|
+
constructorParams: [
|
|
1275
|
+
{
|
|
1276
|
+
name: 'cryptoProvider',
|
|
1277
|
+
type: 'CryptoProvider',
|
|
1278
|
+
optional: false,
|
|
1279
|
+
},
|
|
1280
|
+
],
|
|
937
1281
|
},
|
|
938
1282
|
},
|
|
939
1283
|
interfaces: {},
|
|
@@ -957,7 +1301,12 @@ describe('resolveResourceClassName', () => {
|
|
|
957
1301
|
methodByOperation: new Map([
|
|
958
1302
|
[
|
|
959
1303
|
'GET /webhook_events',
|
|
960
|
-
{
|
|
1304
|
+
{
|
|
1305
|
+
className: 'Webhooks',
|
|
1306
|
+
methodName: 'listWebhookEvents',
|
|
1307
|
+
params: [],
|
|
1308
|
+
returnType: 'void',
|
|
1309
|
+
},
|
|
961
1310
|
],
|
|
962
1311
|
]),
|
|
963
1312
|
httpKeyByMethod: new Map(),
|
|
@@ -1014,7 +1363,15 @@ describe('resolveResourceClassName', () => {
|
|
|
1014
1363
|
spec: { ...emptySpec, services: [collisionService] },
|
|
1015
1364
|
overlayLookup: {
|
|
1016
1365
|
methodByOperation: new Map([
|
|
1017
|
-
[
|
|
1366
|
+
[
|
|
1367
|
+
'GET /webhooks',
|
|
1368
|
+
{
|
|
1369
|
+
className: 'Webhooks',
|
|
1370
|
+
methodName: 'listWebhooks',
|
|
1371
|
+
params: [],
|
|
1372
|
+
returnType: 'void',
|
|
1373
|
+
},
|
|
1374
|
+
],
|
|
1018
1375
|
]),
|
|
1019
1376
|
httpKeyByMethod: new Map(),
|
|
1020
1377
|
interfaceByName: new Map(),
|
|
@@ -1032,7 +1389,13 @@ describe('resolveResourceClassName', () => {
|
|
|
1032
1389
|
name: 'Webhooks',
|
|
1033
1390
|
methods: {},
|
|
1034
1391
|
properties: {},
|
|
1035
|
-
constructorParams: [
|
|
1392
|
+
constructorParams: [
|
|
1393
|
+
{
|
|
1394
|
+
name: 'cryptoProvider',
|
|
1395
|
+
type: 'CryptoProvider',
|
|
1396
|
+
optional: false,
|
|
1397
|
+
},
|
|
1398
|
+
],
|
|
1036
1399
|
},
|
|
1037
1400
|
},
|
|
1038
1401
|
interfaces: {},
|
|
@@ -1090,7 +1453,13 @@ describe('hasCompatibleConstructor', () => {
|
|
|
1090
1453
|
name: 'Webhooks',
|
|
1091
1454
|
methods: {},
|
|
1092
1455
|
properties: {},
|
|
1093
|
-
constructorParams: [
|
|
1456
|
+
constructorParams: [
|
|
1457
|
+
{
|
|
1458
|
+
name: 'cryptoProvider',
|
|
1459
|
+
type: 'CryptoProvider',
|
|
1460
|
+
optional: false,
|
|
1461
|
+
},
|
|
1462
|
+
],
|
|
1094
1463
|
},
|
|
1095
1464
|
},
|
|
1096
1465
|
interfaces: {},
|
|
@@ -1192,7 +1561,14 @@ describe('partial service coverage', () => {
|
|
|
1192
1561
|
AuditLogs: {
|
|
1193
1562
|
name: 'AuditLogs',
|
|
1194
1563
|
methods: {
|
|
1195
|
-
createEvent: [
|
|
1564
|
+
createEvent: [
|
|
1565
|
+
{
|
|
1566
|
+
name: 'createEvent',
|
|
1567
|
+
params: [],
|
|
1568
|
+
returnType: 'AuditLogEvent',
|
|
1569
|
+
async: true,
|
|
1570
|
+
},
|
|
1571
|
+
],
|
|
1196
1572
|
},
|
|
1197
1573
|
properties: {},
|
|
1198
1574
|
constructorParams: [{ name: 'workos', type: 'WorkOS', optional: false }],
|
|
@@ -1211,7 +1587,233 @@ describe('partial service coverage', () => {
|
|
|
1211
1587
|
|
|
1212
1588
|
// Should generate method for uncovered operation
|
|
1213
1589
|
expect(content).toContain('async getRetention');
|
|
1214
|
-
// Should
|
|
1215
|
-
expect(content).
|
|
1590
|
+
// Should also generate covered operation so the merger can apply JSDoc
|
|
1591
|
+
expect(content).toContain('async createEvent');
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
it('generates resource class for fully covered services to provide JSDoc', () => {
|
|
1595
|
+
const services: Service[] = [
|
|
1596
|
+
{
|
|
1597
|
+
name: 'Permissions',
|
|
1598
|
+
operations: [
|
|
1599
|
+
{
|
|
1600
|
+
name: 'listPermissions',
|
|
1601
|
+
description: 'List all permissions.',
|
|
1602
|
+
httpMethod: 'get',
|
|
1603
|
+
path: '/authorization/permissions',
|
|
1604
|
+
pathParams: [],
|
|
1605
|
+
queryParams: [],
|
|
1606
|
+
headerParams: [],
|
|
1607
|
+
response: { kind: 'model', name: 'PermissionList' },
|
|
1608
|
+
errors: [{ statusCode: 404 }],
|
|
1609
|
+
injectIdempotencyKey: false,
|
|
1610
|
+
},
|
|
1611
|
+
],
|
|
1612
|
+
},
|
|
1613
|
+
];
|
|
1614
|
+
|
|
1615
|
+
const ctxCovered: EmitterContext = {
|
|
1616
|
+
...ctx,
|
|
1617
|
+
spec: { ...emptySpec, services, models: [] },
|
|
1618
|
+
overlayLookup: {
|
|
1619
|
+
methodByOperation: new Map([
|
|
1620
|
+
[
|
|
1621
|
+
'GET /authorization/permissions',
|
|
1622
|
+
{
|
|
1623
|
+
className: 'Permissions',
|
|
1624
|
+
methodName: 'listPermissions',
|
|
1625
|
+
params: [],
|
|
1626
|
+
returnType: 'void',
|
|
1627
|
+
},
|
|
1628
|
+
],
|
|
1629
|
+
]),
|
|
1630
|
+
httpKeyByMethod: new Map(),
|
|
1631
|
+
interfaceByName: new Map(),
|
|
1632
|
+
typeAliasByName: new Map(),
|
|
1633
|
+
requiredExports: new Map(),
|
|
1634
|
+
modelNameByIR: new Map(),
|
|
1635
|
+
fileBySymbol: new Map(),
|
|
1636
|
+
},
|
|
1637
|
+
apiSurface: {
|
|
1638
|
+
language: 'node',
|
|
1639
|
+
extractedFrom: 'test',
|
|
1640
|
+
extractedAt: '2024-01-01',
|
|
1641
|
+
classes: {
|
|
1642
|
+
Permissions: {
|
|
1643
|
+
name: 'Permissions',
|
|
1644
|
+
methods: {
|
|
1645
|
+
listPermissions: [
|
|
1646
|
+
{
|
|
1647
|
+
name: 'listPermissions',
|
|
1648
|
+
params: [],
|
|
1649
|
+
returnType: 'void',
|
|
1650
|
+
async: true,
|
|
1651
|
+
},
|
|
1652
|
+
],
|
|
1653
|
+
},
|
|
1654
|
+
properties: {},
|
|
1655
|
+
constructorParams: [{ name: 'workos', type: 'WorkOS', optional: false }],
|
|
1656
|
+
},
|
|
1657
|
+
},
|
|
1658
|
+
interfaces: {},
|
|
1659
|
+
typeAliases: {},
|
|
1660
|
+
enums: {},
|
|
1661
|
+
exports: {},
|
|
1662
|
+
},
|
|
1663
|
+
};
|
|
1664
|
+
|
|
1665
|
+
const files = generateResources(services, ctxCovered);
|
|
1666
|
+
expect(files.length).toBe(1);
|
|
1667
|
+
const content = files[0].content;
|
|
1668
|
+
// Should contain JSDoc with description from the spec
|
|
1669
|
+
expect(content).toContain('List all permissions.');
|
|
1670
|
+
// skipIfExists should remain true for covered services
|
|
1671
|
+
expect(files[0].skipIfExists).toBe(true);
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
it('reconciles method names against api-surface using word-set matching', () => {
|
|
1675
|
+
const services: Service[] = [
|
|
1676
|
+
{
|
|
1677
|
+
name: 'Authorization',
|
|
1678
|
+
operations: [
|
|
1679
|
+
{
|
|
1680
|
+
name: 'listRolesOrganizations',
|
|
1681
|
+
httpMethod: 'get',
|
|
1682
|
+
path: '/authorization/organizations/{organizationId}/roles',
|
|
1683
|
+
pathParams: [
|
|
1684
|
+
{
|
|
1685
|
+
name: 'organizationId',
|
|
1686
|
+
type: { kind: 'primitive', type: 'string' },
|
|
1687
|
+
required: true,
|
|
1688
|
+
},
|
|
1689
|
+
],
|
|
1690
|
+
queryParams: [],
|
|
1691
|
+
headerParams: [],
|
|
1692
|
+
response: { kind: 'model', name: 'RoleList' },
|
|
1693
|
+
errors: [],
|
|
1694
|
+
pagination: {
|
|
1695
|
+
strategy: 'cursor' as const,
|
|
1696
|
+
param: 'after',
|
|
1697
|
+
itemType: { kind: 'model' as const, name: 'RoleList' },
|
|
1698
|
+
},
|
|
1699
|
+
injectIdempotencyKey: false,
|
|
1700
|
+
},
|
|
1701
|
+
],
|
|
1702
|
+
},
|
|
1703
|
+
];
|
|
1704
|
+
|
|
1705
|
+
const ctxRecon: EmitterContext = {
|
|
1706
|
+
...ctx,
|
|
1707
|
+
spec: {
|
|
1708
|
+
...emptySpec,
|
|
1709
|
+
services,
|
|
1710
|
+
models: [{ name: 'RoleList', fields: [] }],
|
|
1711
|
+
},
|
|
1712
|
+
overlayLookup: {
|
|
1713
|
+
methodByOperation: new Map(), // no overlay mapping
|
|
1714
|
+
httpKeyByMethod: new Map(),
|
|
1715
|
+
interfaceByName: new Map(),
|
|
1716
|
+
typeAliasByName: new Map(),
|
|
1717
|
+
requiredExports: new Map(),
|
|
1718
|
+
modelNameByIR: new Map(),
|
|
1719
|
+
fileBySymbol: new Map(),
|
|
1720
|
+
},
|
|
1721
|
+
apiSurface: {
|
|
1722
|
+
language: 'node',
|
|
1723
|
+
extractedFrom: 'test',
|
|
1724
|
+
extractedAt: '2024-01-01',
|
|
1725
|
+
classes: {
|
|
1726
|
+
Authorization: {
|
|
1727
|
+
name: 'Authorization',
|
|
1728
|
+
methods: {
|
|
1729
|
+
listOrganizationRoles: [
|
|
1730
|
+
{
|
|
1731
|
+
name: 'listOrganizationRoles',
|
|
1732
|
+
params: [],
|
|
1733
|
+
returnType: 'void',
|
|
1734
|
+
async: true,
|
|
1735
|
+
},
|
|
1736
|
+
],
|
|
1737
|
+
},
|
|
1738
|
+
properties: {},
|
|
1739
|
+
constructorParams: [{ name: 'workos', type: 'WorkOS', optional: false }],
|
|
1740
|
+
},
|
|
1741
|
+
},
|
|
1742
|
+
interfaces: {},
|
|
1743
|
+
typeAliases: {},
|
|
1744
|
+
enums: {},
|
|
1745
|
+
exports: {},
|
|
1746
|
+
},
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
const files = generateResources(services, ctxRecon);
|
|
1750
|
+
expect(files.length).toBe(1);
|
|
1751
|
+
const content = files[0].content;
|
|
1752
|
+
// Should use reconciled name from api-surface, not spec-derived name
|
|
1753
|
+
expect(content).toContain('async listOrganizationRoles');
|
|
1754
|
+
expect(content).not.toContain('async listRolesOrganizations');
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
it('deduplicates method names for operations on different paths', () => {
|
|
1758
|
+
const services: Service[] = [
|
|
1759
|
+
{
|
|
1760
|
+
name: 'Organizations',
|
|
1761
|
+
operations: [
|
|
1762
|
+
{
|
|
1763
|
+
name: 'create',
|
|
1764
|
+
httpMethod: 'post',
|
|
1765
|
+
path: '/organization_domains',
|
|
1766
|
+
pathParams: [],
|
|
1767
|
+
queryParams: [],
|
|
1768
|
+
headerParams: [],
|
|
1769
|
+
requestBody: { kind: 'model', name: 'CreateOrgDomain' },
|
|
1770
|
+
response: { kind: 'model', name: 'OrgDomain' },
|
|
1771
|
+
errors: [],
|
|
1772
|
+
injectIdempotencyKey: false,
|
|
1773
|
+
},
|
|
1774
|
+
{
|
|
1775
|
+
name: 'create',
|
|
1776
|
+
httpMethod: 'post',
|
|
1777
|
+
path: '/organizations',
|
|
1778
|
+
pathParams: [],
|
|
1779
|
+
queryParams: [],
|
|
1780
|
+
headerParams: [],
|
|
1781
|
+
requestBody: { kind: 'model', name: 'CreateOrg' },
|
|
1782
|
+
response: { kind: 'model', name: 'Organization' },
|
|
1783
|
+
errors: [],
|
|
1784
|
+
injectIdempotencyKey: false,
|
|
1785
|
+
},
|
|
1786
|
+
],
|
|
1787
|
+
},
|
|
1788
|
+
];
|
|
1789
|
+
|
|
1790
|
+
const ctxDedup: EmitterContext = {
|
|
1791
|
+
...ctx,
|
|
1792
|
+
spec: {
|
|
1793
|
+
...emptySpec,
|
|
1794
|
+
services,
|
|
1795
|
+
models: [
|
|
1796
|
+
{ name: 'CreateOrgDomain', fields: [] },
|
|
1797
|
+
{ name: 'OrgDomain', fields: [] },
|
|
1798
|
+
{ name: 'CreateOrg', fields: [] },
|
|
1799
|
+
{ name: 'Organization', fields: [] },
|
|
1800
|
+
],
|
|
1801
|
+
},
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
const files = generateResources(services, ctxDedup);
|
|
1805
|
+
expect(files.length).toBe(1);
|
|
1806
|
+
const content = files[0].content;
|
|
1807
|
+
// The best-scoring plan keeps the name; the other gets disambiguated.
|
|
1808
|
+
// "create" matches "organizations" path better (the word "create" doesn't
|
|
1809
|
+
// appear in either path, but scoring is equal — first wins).
|
|
1810
|
+
// The other gets a path suffix.
|
|
1811
|
+
const createMatches = content.match(/async create\b/g);
|
|
1812
|
+
// At most one un-suffixed "create"
|
|
1813
|
+
expect(createMatches?.length ?? 0).toBeLessThanOrEqual(1);
|
|
1814
|
+
// The two methods should have different names
|
|
1815
|
+
const methodNames = [...content.matchAll(/async (\w+)\(/g)].map((m) => m[1]);
|
|
1816
|
+
const createMethods = methodNames.filter((n) => n.toLowerCase().startsWith('create'));
|
|
1817
|
+
expect(new Set(createMethods).size).toBe(createMethods.length); // all unique
|
|
1216
1818
|
});
|
|
1217
1819
|
});
|