@workos/oagen-emitters 0.4.0 → 0.5.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/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lint.yml +1 -1
- package/.github/workflows/release-please.yml +2 -2
- package/.github/workflows/release.yml +1 -1
- package/.husky/pre-push +11 -0
- package/.node-version +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +8 -0
- package/README.md +35 -224
- package/dist/index.d.mts +9 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -15234
- package/dist/plugin-BSop9f9z.mjs +21471 -0
- package/dist/plugin-BSop9f9z.mjs.map +1 -0
- package/dist/plugin.d.mts +7 -0
- package/dist/plugin.d.mts.map +1 -0
- package/dist/plugin.mjs +2 -0
- package/docs/sdk-architecture/dotnet.md +5 -5
- package/oagen.config.ts +5 -373
- package/package.json +10 -34
- package/src/dotnet/index.ts +6 -4
- package/src/dotnet/models.ts +58 -82
- package/src/dotnet/naming.ts +44 -6
- package/src/dotnet/resources.ts +350 -29
- package/src/dotnet/tests.ts +44 -24
- package/src/dotnet/type-map.ts +44 -17
- package/src/dotnet/wrappers.ts +21 -10
- package/src/go/client.ts +35 -3
- package/src/go/enums.ts +4 -0
- package/src/go/index.ts +10 -5
- package/src/go/models.ts +6 -1
- package/src/go/resources.ts +534 -73
- package/src/go/tests.ts +39 -3
- package/src/go/type-map.ts +8 -3
- package/src/go/wrappers.ts +79 -21
- package/src/index.ts +14 -0
- package/src/kotlin/client.ts +7 -2
- package/src/kotlin/enums.ts +30 -3
- package/src/kotlin/models.ts +97 -6
- package/src/kotlin/naming.ts +7 -1
- package/src/kotlin/resources.ts +370 -39
- package/src/kotlin/tests.ts +120 -6
- package/src/node/client.ts +38 -11
- package/src/node/field-plan.ts +12 -14
- package/src/node/fixtures.ts +39 -3
- package/src/node/models.ts +281 -37
- package/src/node/resources.ts +156 -52
- package/src/node/tests.ts +76 -27
- package/src/node/type-map.ts +1 -31
- package/src/node/utils.ts +96 -6
- package/src/node/wrappers.ts +31 -1
- package/src/php/models.ts +0 -33
- package/src/php/resources.ts +199 -18
- package/src/php/tests.ts +26 -2
- package/src/php/type-map.ts +16 -2
- package/src/php/wrappers.ts +6 -2
- package/src/plugin.ts +50 -0
- package/src/python/client.ts +13 -3
- package/src/python/enums.ts +28 -3
- package/src/python/index.ts +35 -27
- package/src/python/models.ts +138 -1
- package/src/python/resources.ts +234 -17
- package/src/python/tests.ts +260 -16
- package/src/python/type-map.ts +16 -2
- package/src/ruby/client.ts +238 -0
- package/src/ruby/enums.ts +149 -0
- package/src/ruby/index.ts +93 -0
- package/src/ruby/manifest.ts +35 -0
- package/src/ruby/models.ts +360 -0
- package/src/ruby/naming.ts +187 -0
- package/src/ruby/rbi.ts +313 -0
- package/src/ruby/resources.ts +799 -0
- package/src/ruby/tests.ts +459 -0
- package/src/ruby/type-map.ts +97 -0
- package/src/ruby/wrappers.ts +161 -0
- package/src/shared/model-utils.ts +131 -7
- package/src/shared/naming-utils.ts +36 -0
- package/src/shared/non-spec-services.ts +13 -0
- package/src/shared/resolved-ops.ts +75 -1
- package/test/dotnet/client.test.ts +2 -2
- package/test/dotnet/models.test.ts +7 -9
- package/test/dotnet/resources.test.ts +135 -3
- package/test/dotnet/tests.test.ts +5 -5
- package/test/entrypoint.test.ts +89 -0
- package/test/go/client.test.ts +6 -6
- package/test/go/resources.test.ts +156 -7
- package/test/kotlin/models.test.ts +1 -1
- package/test/kotlin/resources.test.ts +210 -0
- package/test/node/models.test.ts +134 -1
- package/test/node/resources.test.ts +134 -26
- package/test/node/utils.test.ts +140 -0
- package/test/php/models.test.ts +5 -4
- package/test/php/resources.test.ts +66 -1
- package/test/plugin.test.ts +50 -0
- package/test/python/client.test.ts +56 -0
- package/test/python/models.test.ts +99 -0
- package/test/python/resources.test.ts +294 -0
- package/test/python/tests.test.ts +91 -0
- package/test/ruby/client.test.ts +81 -0
- package/test/ruby/resources.test.ts +386 -0
- package/test/shared/resolved-ops.test.ts +122 -0
- package/tsdown.config.ts +1 -1
- package/dist/index.mjs.map +0 -1
- package/scripts/generate-php.js +0 -13
- package/scripts/git-push-with-published-oagen.sh +0 -21
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { defaultSdkBehavior, type ApiSpec, type EmitterContext, type Service } from '@workos/oagen';
|
|
3
|
+
import { generateResources } from '../../src/kotlin/resources.js';
|
|
4
|
+
import { generateEnums } from '../../src/kotlin/enums.js';
|
|
5
|
+
|
|
6
|
+
const baseSpec: ApiSpec = {
|
|
7
|
+
name: 'Test',
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
baseUrl: 'https://api.workos.com',
|
|
10
|
+
services: [],
|
|
11
|
+
models: [
|
|
12
|
+
{
|
|
13
|
+
name: 'AuthenticateResponse',
|
|
14
|
+
fields: [{ name: 'access_token', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'SSOTokenResponse',
|
|
18
|
+
fields: [{ name: 'access_token', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
enums: [],
|
|
22
|
+
sdk: defaultSdkBehavior(),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function ctxFor(services: Service[], enums = baseSpec.enums): EmitterContext {
|
|
26
|
+
return {
|
|
27
|
+
namespace: 'workos',
|
|
28
|
+
namespacePascal: 'WorkOS',
|
|
29
|
+
spec: { ...baseSpec, services, enums },
|
|
30
|
+
resolvedOperations: services.flatMap((service) =>
|
|
31
|
+
service.operations.map((operation) => ({
|
|
32
|
+
service,
|
|
33
|
+
operation,
|
|
34
|
+
methodName: operation.name,
|
|
35
|
+
mountOn: service.name,
|
|
36
|
+
defaults: {},
|
|
37
|
+
inferFromClient: [],
|
|
38
|
+
urlBuilder: false,
|
|
39
|
+
})),
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('kotlin/resources', () => {
|
|
45
|
+
it('collapses duplicated query/body params into a single Kotlin parameter', () => {
|
|
46
|
+
const services: Service[] = [
|
|
47
|
+
{
|
|
48
|
+
name: 'SSO',
|
|
49
|
+
operations: [
|
|
50
|
+
{
|
|
51
|
+
name: 'getProfileAndToken',
|
|
52
|
+
httpMethod: 'post',
|
|
53
|
+
path: '/sso/token',
|
|
54
|
+
pathParams: [],
|
|
55
|
+
queryParams: [{ name: 'code', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
56
|
+
headerParams: [],
|
|
57
|
+
requestBody: { kind: 'model', name: 'GetProfileAndTokenRequest' },
|
|
58
|
+
response: { kind: 'model', name: 'SSOTokenResponse' },
|
|
59
|
+
errors: [],
|
|
60
|
+
injectIdempotencyKey: false,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
const spec = {
|
|
66
|
+
...baseSpec,
|
|
67
|
+
services,
|
|
68
|
+
models: [
|
|
69
|
+
...baseSpec.models,
|
|
70
|
+
{
|
|
71
|
+
name: 'GetProfileAndTokenRequest',
|
|
72
|
+
fields: [{ name: 'code', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
const files = generateResources(services, { ...ctxFor(services), spec: spec as ApiSpec });
|
|
77
|
+
const ssoFile = files.find((file) => file.path.endsWith('/Sso.kt'));
|
|
78
|
+
expect(ssoFile).toBeDefined();
|
|
79
|
+
expect(ssoFile!.content).toContain('fun getProfileAndToken(');
|
|
80
|
+
expect(ssoFile!.content).toContain('code: String');
|
|
81
|
+
expect(ssoFile!.content).not.toContain('bodyCode');
|
|
82
|
+
expect(ssoFile!.content).toContain('params += "code" to code');
|
|
83
|
+
expect(ssoFile!.content).toContain('"code" to code');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('emits a shared authenticate helper for user management authenticate variants', () => {
|
|
87
|
+
const services: Service[] = [
|
|
88
|
+
{
|
|
89
|
+
name: 'UserManagement',
|
|
90
|
+
operations: [
|
|
91
|
+
{
|
|
92
|
+
name: 'authenticateWithPassword',
|
|
93
|
+
httpMethod: 'post',
|
|
94
|
+
path: '/user_management/authenticate',
|
|
95
|
+
pathParams: [],
|
|
96
|
+
queryParams: [],
|
|
97
|
+
headerParams: [],
|
|
98
|
+
requestBody: { kind: 'model', name: 'AuthenticatePasswordRequest' },
|
|
99
|
+
response: { kind: 'model', name: 'AuthenticateResponse' },
|
|
100
|
+
errors: [],
|
|
101
|
+
injectIdempotencyKey: false,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
const spec = {
|
|
107
|
+
...baseSpec,
|
|
108
|
+
services,
|
|
109
|
+
models: [
|
|
110
|
+
...baseSpec.models,
|
|
111
|
+
{
|
|
112
|
+
name: 'AuthenticatePasswordRequest',
|
|
113
|
+
fields: [
|
|
114
|
+
{ name: 'email', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
115
|
+
{ name: 'password', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
const files = generateResources(services, { ...ctxFor(services), spec: spec as ApiSpec });
|
|
121
|
+
const userManagementFile = files.find((file) => file.path.endsWith('/UserManagement.kt'));
|
|
122
|
+
expect(userManagementFile).toBeDefined();
|
|
123
|
+
expect(userManagementFile!.content).toContain('private fun authenticate(');
|
|
124
|
+
expect(userManagementFile!.content).toContain('grantType = "authorization_code"');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('renames package-level parameter group helpers to avoid Role/Password collisions', () => {
|
|
128
|
+
const services: Service[] = [
|
|
129
|
+
{
|
|
130
|
+
name: 'UserManagement',
|
|
131
|
+
operations: [
|
|
132
|
+
{
|
|
133
|
+
name: 'createUser',
|
|
134
|
+
httpMethod: 'post',
|
|
135
|
+
path: '/users',
|
|
136
|
+
pathParams: [],
|
|
137
|
+
queryParams: [],
|
|
138
|
+
headerParams: [],
|
|
139
|
+
requestBody: { kind: 'model', name: 'CreateUserRequest' },
|
|
140
|
+
response: { kind: 'model', name: 'AuthenticateResponse' },
|
|
141
|
+
errors: [],
|
|
142
|
+
injectIdempotencyKey: false,
|
|
143
|
+
parameterGroups: [
|
|
144
|
+
{
|
|
145
|
+
name: 'Password',
|
|
146
|
+
optional: true,
|
|
147
|
+
variants: [
|
|
148
|
+
{
|
|
149
|
+
name: 'Plaintext',
|
|
150
|
+
parameters: [{ name: 'password', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'Role',
|
|
156
|
+
optional: true,
|
|
157
|
+
variants: [
|
|
158
|
+
{
|
|
159
|
+
name: 'Single',
|
|
160
|
+
parameters: [{ name: 'role_slug', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
const spec = {
|
|
170
|
+
...baseSpec,
|
|
171
|
+
services,
|
|
172
|
+
models: [
|
|
173
|
+
...baseSpec.models,
|
|
174
|
+
{
|
|
175
|
+
name: 'CreateUserRequest',
|
|
176
|
+
fields: [
|
|
177
|
+
{ name: 'password', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
178
|
+
{ name: 'role_slug', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
const files = generateResources(services, { ...ctxFor(services), spec: spec as ApiSpec });
|
|
184
|
+
const userManagementFile = files.find((file) => file.path.endsWith('/UserManagement.kt'));
|
|
185
|
+
expect(userManagementFile).toBeDefined();
|
|
186
|
+
expect(userManagementFile!.content).toContain('sealed class CreateUserPassword');
|
|
187
|
+
expect(userManagementFile!.content).toContain('sealed class CreateUserRole');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('collapses asc/desc order enums into SortOrder', () => {
|
|
191
|
+
const enums = [
|
|
192
|
+
{
|
|
193
|
+
name: 'EventsOrder',
|
|
194
|
+
values: [{ value: 'asc' }, { value: 'desc' }],
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'OrganizationsOrder',
|
|
198
|
+
values: [{ value: 'asc' }, { value: 'desc' }],
|
|
199
|
+
},
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
const files = generateEnums(enums as never, ctxFor([], enums as never));
|
|
203
|
+
const sortOrder = files.find((file) => file.path.endsWith('/SortOrder.kt'));
|
|
204
|
+
const aliases = files.filter((file) => file.path.endsWith('Order.kt') && !file.path.endsWith('/SortOrder.kt'));
|
|
205
|
+
|
|
206
|
+
expect(sortOrder).toBeDefined();
|
|
207
|
+
expect(sortOrder!.content).toContain('enum class SortOrder');
|
|
208
|
+
expect(aliases.length).toBeLessThanOrEqual(1);
|
|
209
|
+
});
|
|
210
|
+
});
|
package/test/node/models.test.ts
CHANGED
|
@@ -182,6 +182,23 @@ describe('generateModels', () => {
|
|
|
182
182
|
});
|
|
183
183
|
|
|
184
184
|
it('handles generic type params', () => {
|
|
185
|
+
const service: Service = {
|
|
186
|
+
name: 'DirectorySync',
|
|
187
|
+
operations: [
|
|
188
|
+
{
|
|
189
|
+
name: 'getDirectoryUser',
|
|
190
|
+
httpMethod: 'get',
|
|
191
|
+
path: '/directory_users/{id}',
|
|
192
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
193
|
+
queryParams: [],
|
|
194
|
+
headerParams: [],
|
|
195
|
+
response: { kind: 'model', name: 'DirectoryUser' },
|
|
196
|
+
errors: [],
|
|
197
|
+
injectIdempotencyKey: false,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
|
|
185
202
|
const models: Model[] = [
|
|
186
203
|
{
|
|
187
204
|
name: 'DirectoryUser',
|
|
@@ -204,7 +221,12 @@ describe('generateModels', () => {
|
|
|
204
221
|
},
|
|
205
222
|
];
|
|
206
223
|
|
|
207
|
-
const
|
|
224
|
+
const ctxWithServices: EmitterContext = {
|
|
225
|
+
...ctx,
|
|
226
|
+
spec: { ...emptySpec, services: [service], models },
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const files = generateModels(models, ctxWithServices);
|
|
208
230
|
expect(files[0].content).toContain('export interface DirectoryUser<TCustom = Record<string, any>> {');
|
|
209
231
|
expect(files[0].content).toContain('export interface DirectoryUserResponse<TCustom = Record<string, any>> {');
|
|
210
232
|
});
|
|
@@ -733,6 +755,23 @@ describe('model deduplication', () => {
|
|
|
733
755
|
errors: [],
|
|
734
756
|
injectIdempotencyKey: false,
|
|
735
757
|
},
|
|
758
|
+
{
|
|
759
|
+
name: 'getOrganizationRole',
|
|
760
|
+
httpMethod: 'get',
|
|
761
|
+
path: '/organization_roles/{id}',
|
|
762
|
+
pathParams: [
|
|
763
|
+
{
|
|
764
|
+
name: 'id',
|
|
765
|
+
type: { kind: 'primitive', type: 'string' },
|
|
766
|
+
required: true,
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
queryParams: [],
|
|
770
|
+
headerParams: [],
|
|
771
|
+
response: { kind: 'model', name: 'OrganizationRole' },
|
|
772
|
+
errors: [],
|
|
773
|
+
injectIdempotencyKey: false,
|
|
774
|
+
},
|
|
736
775
|
],
|
|
737
776
|
};
|
|
738
777
|
|
|
@@ -794,4 +833,98 @@ describe('model deduplication', () => {
|
|
|
794
833
|
expect(files[1].content).toContain('export type OrganizationRole = EnvironmentRole');
|
|
795
834
|
expect(files[1].content).toContain('export type OrganizationRoleResponse = EnvironmentRoleResponse');
|
|
796
835
|
});
|
|
836
|
+
|
|
837
|
+
it('generates Date type for date-time fields even when baseline says string', () => {
|
|
838
|
+
const service: Service = {
|
|
839
|
+
name: 'Authorization',
|
|
840
|
+
operations: [
|
|
841
|
+
{
|
|
842
|
+
name: 'getRoleAssignment',
|
|
843
|
+
httpMethod: 'get',
|
|
844
|
+
path: '/role_assignments/{id}',
|
|
845
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
846
|
+
queryParams: [],
|
|
847
|
+
headerParams: [],
|
|
848
|
+
response: { kind: 'model', name: 'RoleAssignment' },
|
|
849
|
+
errors: [],
|
|
850
|
+
injectIdempotencyKey: false,
|
|
851
|
+
},
|
|
852
|
+
],
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
const models: Model[] = [
|
|
856
|
+
{
|
|
857
|
+
name: 'RoleAssignment',
|
|
858
|
+
fields: [
|
|
859
|
+
{
|
|
860
|
+
name: 'id',
|
|
861
|
+
type: { kind: 'primitive', type: 'string' },
|
|
862
|
+
required: true,
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
name: 'created_at',
|
|
866
|
+
type: { kind: 'primitive', type: 'string', format: 'date-time' },
|
|
867
|
+
required: true,
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
name: 'updated_at',
|
|
871
|
+
type: { kind: 'primitive', type: 'string', format: 'date-time' },
|
|
872
|
+
required: true,
|
|
873
|
+
},
|
|
874
|
+
// Extra field not in baseline so modelHasNewFields returns true
|
|
875
|
+
// (allowing the dedup test to proceed with generation)
|
|
876
|
+
{
|
|
877
|
+
name: 'role_name',
|
|
878
|
+
type: { kind: 'primitive', type: 'string' },
|
|
879
|
+
required: true,
|
|
880
|
+
},
|
|
881
|
+
],
|
|
882
|
+
},
|
|
883
|
+
];
|
|
884
|
+
|
|
885
|
+
const ctxWithBaseline: EmitterContext = {
|
|
886
|
+
...ctx,
|
|
887
|
+
spec: { ...emptySpec, services: [service], models },
|
|
888
|
+
apiSurface: {
|
|
889
|
+
language: 'node',
|
|
890
|
+
extractedFrom: 'test',
|
|
891
|
+
extractedAt: '2024-01-01',
|
|
892
|
+
classes: {},
|
|
893
|
+
typeAliases: {},
|
|
894
|
+
enums: {},
|
|
895
|
+
exports: {},
|
|
896
|
+
interfaces: {
|
|
897
|
+
RoleAssignment: {
|
|
898
|
+
name: 'RoleAssignment',
|
|
899
|
+
fields: {
|
|
900
|
+
id: { name: 'id', type: 'string', optional: false },
|
|
901
|
+
createdAt: { name: 'createdAt', type: 'string', optional: false },
|
|
902
|
+
updatedAt: { name: 'updatedAt', type: 'string', optional: false },
|
|
903
|
+
},
|
|
904
|
+
extends: [],
|
|
905
|
+
},
|
|
906
|
+
RoleAssignmentResponse: {
|
|
907
|
+
name: 'RoleAssignmentResponse',
|
|
908
|
+
fields: {
|
|
909
|
+
id: { name: 'id', type: 'string', optional: false },
|
|
910
|
+
created_at: { name: 'created_at', type: 'string', optional: false },
|
|
911
|
+
updated_at: { name: 'updated_at', type: 'string', optional: false },
|
|
912
|
+
},
|
|
913
|
+
extends: [],
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
},
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
const files = generateModels(models, ctxWithBaseline);
|
|
920
|
+
const content = files[0].content;
|
|
921
|
+
|
|
922
|
+
// Domain interface should use Date, not string from baseline
|
|
923
|
+
expect(content).toContain(' createdAt: Date;');
|
|
924
|
+
expect(content).toContain(' updatedAt: Date;');
|
|
925
|
+
|
|
926
|
+
// Wire interface should stay as string (JSON native)
|
|
927
|
+
expect(content).toContain(' created_at: string;');
|
|
928
|
+
expect(content).toContain(' updated_at: string;');
|
|
929
|
+
});
|
|
797
930
|
});
|
|
@@ -99,9 +99,9 @@ describe('generateResources', () => {
|
|
|
99
99
|
const files = generateResources(services, ctx);
|
|
100
100
|
const content = files[0].content;
|
|
101
101
|
|
|
102
|
-
// Should have AutoPaginatable
|
|
103
|
-
expect(content).toContain("import
|
|
104
|
-
expect(content).toContain("import {
|
|
102
|
+
// Should have AutoPaginatable value import and fetchAndDeserialize import
|
|
103
|
+
expect(content).toContain("import { AutoPaginatable } from '../common/utils/pagination'");
|
|
104
|
+
expect(content).toContain("import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize'");
|
|
105
105
|
// Options interface lives in its own file under interfaces/ so the
|
|
106
106
|
// per-service barrel picks it up.
|
|
107
107
|
expect(content).toContain(
|
|
@@ -120,10 +120,11 @@ describe('generateResources', () => {
|
|
|
120
120
|
expect(content).toContain('Promise<AutoPaginatable<Organization, ListOrganizationsOptions>>');
|
|
121
121
|
|
|
122
122
|
// `domains` has the same camelCase and snake_case spelling, so no wire
|
|
123
|
-
// serializer should be emitted
|
|
124
|
-
// with just options (no 5th arg).
|
|
123
|
+
// serializer should be emitted; options are passed directly.
|
|
125
124
|
expect(content).not.toContain('serializeListOrganizationsOptions');
|
|
126
|
-
expect(content).
|
|
125
|
+
expect(content).toContain('new AutoPaginatable(');
|
|
126
|
+
expect(content).toContain('fetchAndDeserialize<OrganizationResponse, Organization>');
|
|
127
|
+
expect(content).toContain('options,');
|
|
127
128
|
});
|
|
128
129
|
|
|
129
130
|
it('emits wire-options serializer for paginated list with camelCase filter fields', () => {
|
|
@@ -187,8 +188,9 @@ describe('generateResources', () => {
|
|
|
187
188
|
expect(content).toContain('wire.organization_id = options.organizationId');
|
|
188
189
|
expect(content).not.toContain('wire.organizationId');
|
|
189
190
|
|
|
190
|
-
//
|
|
191
|
-
expect(content).
|
|
191
|
+
// fetchAndDeserialize is invoked with the serialized options.
|
|
192
|
+
expect(content).toContain('serializeListApplicationsOptions(options)');
|
|
193
|
+
expect(content).toContain('new AutoPaginatable(');
|
|
192
194
|
});
|
|
193
195
|
|
|
194
196
|
it('uses item type not list wrapper type for paginated methods', () => {
|
|
@@ -230,7 +232,7 @@ describe('generateResources', () => {
|
|
|
230
232
|
const content = files[0].content;
|
|
231
233
|
|
|
232
234
|
// Should use item type (Connection) not list wrapper (ConnectionList)
|
|
233
|
-
expect(content).toContain('
|
|
235
|
+
expect(content).toContain('fetchAndDeserialize<ConnectionResponse, Connection>');
|
|
234
236
|
expect(content).toContain('deserializeConnection');
|
|
235
237
|
expect(content).toContain('Promise<AutoPaginatable<Connection,');
|
|
236
238
|
|
|
@@ -1085,7 +1087,7 @@ describe('generateResources', () => {
|
|
|
1085
1087
|
expect(content).toContain("case 'refresh_token': return serializeTokenByRefresh(payload)");
|
|
1086
1088
|
});
|
|
1087
1089
|
|
|
1088
|
-
it('uses
|
|
1090
|
+
it('uses AutoPaginatable pattern in paginated methods', () => {
|
|
1089
1091
|
const services: Service[] = [
|
|
1090
1092
|
{
|
|
1091
1093
|
name: 'Connections',
|
|
@@ -1114,9 +1116,9 @@ describe('generateResources', () => {
|
|
|
1114
1116
|
const files = generateResources(services, ctx);
|
|
1115
1117
|
const content = files[0].content;
|
|
1116
1118
|
|
|
1117
|
-
// Should use
|
|
1118
|
-
expect(content).toContain('
|
|
1119
|
-
expect(content).toContain('
|
|
1119
|
+
// Should use AutoPaginatable + fetchAndDeserialize pattern for paginated methods
|
|
1120
|
+
expect(content).toContain('new AutoPaginatable(');
|
|
1121
|
+
expect(content).toContain('fetchAndDeserialize<ConnectionResponse, Connection>');
|
|
1120
1122
|
expect(content).toContain('deserializeConnection');
|
|
1121
1123
|
});
|
|
1122
1124
|
|
|
@@ -1407,10 +1409,8 @@ describe('generateResources', () => {
|
|
|
1407
1409
|
};
|
|
1408
1410
|
|
|
1409
1411
|
const files = generateResources(services, overlayCtx);
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
// skipIfExists should stay true because all methods exist in baseline
|
|
1413
|
-
expect(files[0].skipIfExists).toBe(true);
|
|
1412
|
+
// Fully covered services with no new methods are skipped entirely
|
|
1413
|
+
expect(files.length).toBe(0);
|
|
1414
1414
|
});
|
|
1415
1415
|
|
|
1416
1416
|
it('removes skipIfExists for purely oagen-managed services (no baseline)', () => {
|
|
@@ -1848,7 +1848,7 @@ describe('partial service coverage', () => {
|
|
|
1848
1848
|
expect(content).toContain('async createEvent');
|
|
1849
1849
|
});
|
|
1850
1850
|
|
|
1851
|
-
it('
|
|
1851
|
+
it('skips fully covered services with no new methods', () => {
|
|
1852
1852
|
const services: Service[] = [
|
|
1853
1853
|
{
|
|
1854
1854
|
name: 'Permissions',
|
|
@@ -1920,14 +1920,9 @@ describe('partial service coverage', () => {
|
|
|
1920
1920
|
};
|
|
1921
1921
|
|
|
1922
1922
|
const files = generateResources(services, ctxCovered);
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
// and handles @deprecated preservation, so the emitter always provides
|
|
1927
|
-
// docstrings for the merger to work with.
|
|
1928
|
-
expect(content).toContain('List all permissions.');
|
|
1929
|
-
// skipIfExists should remain true for covered services
|
|
1930
|
-
expect(files[0].skipIfExists).toBe(true);
|
|
1923
|
+
// Fully covered services with no new methods are skipped entirely
|
|
1924
|
+
// (JSDoc-only updates are deferred until a structural change touches the file)
|
|
1925
|
+
expect(files.length).toBe(0);
|
|
1931
1926
|
});
|
|
1932
1927
|
|
|
1933
1928
|
it('uses resolved operation method names when provided', () => {
|
|
@@ -2047,4 +2042,117 @@ describe('partial service coverage', () => {
|
|
|
2047
2042
|
const createMethods = methodNames.filter((n) => n.toLowerCase().startsWith('create'));
|
|
2048
2043
|
expect(new Set(createMethods).size).toBe(createMethods.length); // all unique
|
|
2049
2044
|
});
|
|
2045
|
+
|
|
2046
|
+
it('omits @param payload when overlay method has no payload param', () => {
|
|
2047
|
+
const services: Service[] = [
|
|
2048
|
+
{
|
|
2049
|
+
name: 'AuditLogs',
|
|
2050
|
+
operations: [
|
|
2051
|
+
{
|
|
2052
|
+
name: 'createEvent',
|
|
2053
|
+
httpMethod: 'post',
|
|
2054
|
+
path: '/audit_logs/events',
|
|
2055
|
+
pathParams: [],
|
|
2056
|
+
queryParams: [],
|
|
2057
|
+
headerParams: [],
|
|
2058
|
+
requestBody: { kind: 'model', name: 'CreateAuditLogEvent' },
|
|
2059
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
2060
|
+
errors: [],
|
|
2061
|
+
injectIdempotencyKey: false,
|
|
2062
|
+
},
|
|
2063
|
+
],
|
|
2064
|
+
},
|
|
2065
|
+
];
|
|
2066
|
+
|
|
2067
|
+
const overlayCtx: EmitterContext = {
|
|
2068
|
+
namespace: 'workos',
|
|
2069
|
+
namespacePascal: 'WorkOS',
|
|
2070
|
+
spec: { ...emptySpec, services, models: [] },
|
|
2071
|
+
overlayLookup: {
|
|
2072
|
+
methodByOperation: new Map([
|
|
2073
|
+
[
|
|
2074
|
+
'POST /audit_logs/events',
|
|
2075
|
+
{
|
|
2076
|
+
className: 'AuditLogs',
|
|
2077
|
+
methodName: 'createEvent',
|
|
2078
|
+
params: [
|
|
2079
|
+
{ name: 'organization', type: 'string', optional: false },
|
|
2080
|
+
{ name: 'event', type: 'CreateAuditLogEventOptions', optional: false },
|
|
2081
|
+
{ name: 'options', type: 'CreateAuditLogEventRequestOptions', optional: true },
|
|
2082
|
+
],
|
|
2083
|
+
returnType: 'Promise<void>',
|
|
2084
|
+
},
|
|
2085
|
+
],
|
|
2086
|
+
]),
|
|
2087
|
+
httpKeyByMethod: new Map(),
|
|
2088
|
+
interfaceByName: new Map(),
|
|
2089
|
+
typeAliasByName: new Map(),
|
|
2090
|
+
requiredExports: new Map(),
|
|
2091
|
+
modelNameByIR: new Map(),
|
|
2092
|
+
fileBySymbol: new Map(),
|
|
2093
|
+
},
|
|
2094
|
+
};
|
|
2095
|
+
|
|
2096
|
+
const files = generateResources(services, overlayCtx);
|
|
2097
|
+
const content = files[0].content;
|
|
2098
|
+
// Overlay has (organization, event, options) — no payload param
|
|
2099
|
+
expect(content).not.toContain('@param payload');
|
|
2100
|
+
});
|
|
2101
|
+
|
|
2102
|
+
it('documents @param options when overlay folds path params into options', () => {
|
|
2103
|
+
const services: Service[] = [
|
|
2104
|
+
{
|
|
2105
|
+
name: 'FeatureFlags',
|
|
2106
|
+
operations: [
|
|
2107
|
+
{
|
|
2108
|
+
name: 'addFeatureFlagTarget',
|
|
2109
|
+
httpMethod: 'post',
|
|
2110
|
+
path: '/feature-flags/{slug}/targets/{target_id}',
|
|
2111
|
+
pathParams: [
|
|
2112
|
+
{ name: 'slug', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
2113
|
+
{ name: 'target_id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
2114
|
+
],
|
|
2115
|
+
queryParams: [],
|
|
2116
|
+
headerParams: [],
|
|
2117
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
2118
|
+
errors: [],
|
|
2119
|
+
injectIdempotencyKey: false,
|
|
2120
|
+
},
|
|
2121
|
+
],
|
|
2122
|
+
},
|
|
2123
|
+
];
|
|
2124
|
+
|
|
2125
|
+
const overlayCtx: EmitterContext = {
|
|
2126
|
+
namespace: 'workos',
|
|
2127
|
+
namespacePascal: 'WorkOS',
|
|
2128
|
+
spec: { ...emptySpec, services, models: [] },
|
|
2129
|
+
overlayLookup: {
|
|
2130
|
+
methodByOperation: new Map([
|
|
2131
|
+
[
|
|
2132
|
+
'POST /feature-flags/{slug}/targets/{target_id}',
|
|
2133
|
+
{
|
|
2134
|
+
className: 'FeatureFlags',
|
|
2135
|
+
methodName: 'addFlagTarget',
|
|
2136
|
+
params: [{ name: 'options', type: 'AddFlagTargetOptions', optional: false }],
|
|
2137
|
+
returnType: 'Promise<void>',
|
|
2138
|
+
},
|
|
2139
|
+
],
|
|
2140
|
+
]),
|
|
2141
|
+
httpKeyByMethod: new Map(),
|
|
2142
|
+
interfaceByName: new Map(),
|
|
2143
|
+
typeAliasByName: new Map(),
|
|
2144
|
+
requiredExports: new Map(),
|
|
2145
|
+
modelNameByIR: new Map(),
|
|
2146
|
+
fileBySymbol: new Map(),
|
|
2147
|
+
},
|
|
2148
|
+
};
|
|
2149
|
+
|
|
2150
|
+
const files = generateResources(services, overlayCtx);
|
|
2151
|
+
const content = files[0].content;
|
|
2152
|
+
// Path params (slug, targetId) are folded into options — should not appear as top-level @param
|
|
2153
|
+
expect(content).not.toContain('@param slug');
|
|
2154
|
+
expect(content).not.toContain('@param targetId');
|
|
2155
|
+
// Should have @param options since it's in the overlay signature
|
|
2156
|
+
expect(content).toContain('@param options');
|
|
2157
|
+
});
|
|
2050
2158
|
});
|