@workos/oagen-emitters 0.12.1 → 0.12.2
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-pr-title.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/.node-version +1 -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 +1 -1
- package/dist/{plugin-CmfzawTp.mjs → plugin-eCuvoL1T.mjs} +2508 -1474
- package/dist/plugin-eCuvoL1T.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +6 -6
- package/renovate.json +46 -6
- package/src/node/client.ts +19 -32
- package/src/node/enums.ts +67 -30
- package/src/node/errors.ts +2 -8
- package/src/node/field-plan.ts +188 -52
- package/src/node/fixtures.ts +11 -33
- package/src/node/index.ts +345 -20
- package/src/node/live-surface.ts +378 -0
- package/src/node/models.ts +540 -351
- package/src/node/naming.ts +119 -25
- package/src/node/node-overrides.ts +77 -0
- package/src/node/options.ts +41 -0
- package/src/node/resources.ts +455 -46
- package/src/node/sdk-errors.ts +0 -16
- package/src/node/tests.ts +108 -83
- package/src/node/type-map.ts +40 -18
- package/src/node/utils.ts +89 -102
- package/src/node/wrappers.ts +0 -20
- package/test/node/client.test.ts +106 -1201
- package/test/node/enums.test.ts +59 -130
- package/test/node/errors.test.ts +2 -3
- package/test/node/live-surface.test.ts +240 -0
- package/test/node/models.test.ts +396 -765
- package/test/node/naming.test.ts +69 -234
- package/test/node/resources.test.ts +376 -2036
- package/test/node/tests.test.ts +119 -0
- package/test/node/type-map.test.ts +49 -54
- package/test/node/utils.test.ts +29 -80
- package/dist/plugin-CmfzawTp.mjs.map +0 -1
- package/test/node/serializers.test.ts +0 -444
package/test/node/client.test.ts
CHANGED
|
@@ -1,51 +1,15 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { EmitterContext, ApiSpec, Service } from '@workos/oagen';
|
|
3
|
+
import { defaultSdkBehavior } from '@workos/oagen';
|
|
2
4
|
import { generateClient } from '../../src/node/client.js';
|
|
3
5
|
import { isServiceCoveredByExisting } from '../../src/node/utils.js';
|
|
4
|
-
import type { EmitterContext, ApiSpec, Service, Model, Enum } from '@workos/oagen';
|
|
5
|
-
import { defaultSdkBehavior } from '@workos/oagen';
|
|
6
|
-
import type { ApiSurface } from '@workos/oagen/compat';
|
|
7
|
-
|
|
8
|
-
const service: Service = {
|
|
9
|
-
name: 'Organizations',
|
|
10
|
-
operations: [
|
|
11
|
-
{
|
|
12
|
-
name: 'getOrganization',
|
|
13
|
-
httpMethod: 'get',
|
|
14
|
-
path: '/organizations/{id}',
|
|
15
|
-
pathParams: [
|
|
16
|
-
{
|
|
17
|
-
name: 'id',
|
|
18
|
-
type: { kind: 'primitive', type: 'string' },
|
|
19
|
-
required: true,
|
|
20
|
-
},
|
|
21
|
-
],
|
|
22
|
-
queryParams: [],
|
|
23
|
-
headerParams: [],
|
|
24
|
-
response: { kind: 'model', name: 'Organization' },
|
|
25
|
-
errors: [],
|
|
26
|
-
injectIdempotencyKey: false,
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
};
|
|
30
6
|
|
|
31
|
-
const
|
|
32
|
-
name: 'Organization',
|
|
33
|
-
fields: [
|
|
34
|
-
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
35
|
-
{
|
|
36
|
-
name: 'name',
|
|
37
|
-
type: { kind: 'primitive', type: 'string' },
|
|
38
|
-
required: true,
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const spec: ApiSpec = {
|
|
7
|
+
const emptySpec: ApiSpec = {
|
|
44
8
|
name: 'Test',
|
|
45
9
|
version: '1.0.0',
|
|
46
|
-
baseUrl: 'https://api.
|
|
47
|
-
services: [
|
|
48
|
-
models: [
|
|
10
|
+
baseUrl: 'https://api.workos.com',
|
|
11
|
+
services: [],
|
|
12
|
+
models: [],
|
|
49
13
|
enums: [],
|
|
50
14
|
sdk: defaultSdkBehavior(),
|
|
51
15
|
};
|
|
@@ -53,940 +17,140 @@ const spec: ApiSpec = {
|
|
|
53
17
|
const ctx: EmitterContext = {
|
|
54
18
|
namespace: 'workos',
|
|
55
19
|
namespacePascal: 'WorkOS',
|
|
56
|
-
spec,
|
|
20
|
+
spec: emptySpec,
|
|
57
21
|
};
|
|
58
22
|
|
|
59
23
|
describe('generateClient', () => {
|
|
60
24
|
it('generates WorkOS client with resource accessors', () => {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
expect(workosFile!.integrateTarget).not.toBe(false);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('generates barrel exports', () => {
|
|
79
|
-
const files = generateClient(spec, ctx);
|
|
80
|
-
const barrel = files.find((f) => f.path === 'src/index.ts');
|
|
81
|
-
expect(barrel).toBeDefined();
|
|
82
|
-
|
|
83
|
-
const content = barrel!.content;
|
|
84
|
-
expect(content).toContain("export * from './common/exceptions';");
|
|
85
|
-
expect(content).toContain("export { AutoPaginatable } from './common/utils/pagination';");
|
|
86
|
-
expect(content).toContain("export { WorkOS } from './workos';");
|
|
87
|
-
// Service types are now re-exported via the service barrel
|
|
88
|
-
expect(content).toContain("export * from './organizations/interfaces';");
|
|
89
|
-
expect(content).not.toContain('export type { Organization, OrganizationResponse }');
|
|
90
|
-
expect(content).toContain("export { Organizations } from './organizations/organizations';");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('generates per-service barrel files', () => {
|
|
94
|
-
const files = generateClient(spec, ctx);
|
|
95
|
-
const serviceBarrel = files.find((f) => f.path === 'src/organizations/interfaces/index.ts');
|
|
96
|
-
expect(serviceBarrel).toBeDefined();
|
|
97
|
-
|
|
98
|
-
const content = serviceBarrel!.content;
|
|
99
|
-
expect(content).toContain("export * from './organization.interface';");
|
|
100
|
-
expect(serviceBarrel!.skipIfExists).toBe(true);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('does not generate package.json, tsconfig.json, or worker barrel (now hand-maintained)', () => {
|
|
104
|
-
const files = generateClient(spec, ctx);
|
|
105
|
-
expect(files.find((f) => f.path === 'package.json')).toBeUndefined();
|
|
106
|
-
expect(files.find((f) => f.path === 'tsconfig.json')).toBeUndefined();
|
|
107
|
-
expect(files.find((f) => f.path === 'src/index.worker.ts')).toBeUndefined();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('uses overlay-resolved names for imports and accessors', () => {
|
|
111
|
-
const mfaService: Service = {
|
|
112
|
-
name: 'Billing',
|
|
113
|
-
operations: [
|
|
114
|
-
{
|
|
115
|
-
name: 'enrollFactor',
|
|
116
|
-
httpMethod: 'post',
|
|
117
|
-
path: '/auth/factors/enroll',
|
|
118
|
-
pathParams: [],
|
|
119
|
-
queryParams: [],
|
|
120
|
-
headerParams: [],
|
|
121
|
-
response: { kind: 'model', name: 'AuthenticationFactor' },
|
|
122
|
-
errors: [],
|
|
123
|
-
injectIdempotencyKey: true,
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const mfaModel: Model = {
|
|
129
|
-
name: 'AuthenticationFactor',
|
|
130
|
-
fields: [
|
|
131
|
-
{
|
|
132
|
-
name: 'id',
|
|
133
|
-
type: { kind: 'primitive', type: 'string' },
|
|
134
|
-
required: true,
|
|
135
|
-
},
|
|
136
|
-
],
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const overlaySpec: ApiSpec = {
|
|
140
|
-
name: 'Test',
|
|
141
|
-
version: '1.0.0',
|
|
142
|
-
baseUrl: 'https://api.example.com',
|
|
143
|
-
services: [mfaService],
|
|
144
|
-
models: [mfaModel],
|
|
145
|
-
enums: [],
|
|
146
|
-
sdk: defaultSdkBehavior(),
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const overlayCtx: EmitterContext = {
|
|
150
|
-
namespace: 'workos',
|
|
151
|
-
namespacePascal: 'WorkOS',
|
|
152
|
-
spec: overlaySpec,
|
|
153
|
-
overlayLookup: {
|
|
154
|
-
methodByOperation: new Map([
|
|
155
|
-
[
|
|
156
|
-
'POST /auth/factors/enroll',
|
|
157
|
-
{
|
|
158
|
-
className: 'Mfa',
|
|
159
|
-
methodName: 'enrollFactor',
|
|
160
|
-
params: [],
|
|
161
|
-
returnType: 'void',
|
|
162
|
-
},
|
|
163
|
-
],
|
|
164
|
-
]),
|
|
165
|
-
httpKeyByMethod: new Map(),
|
|
166
|
-
interfaceByName: new Map(),
|
|
167
|
-
typeAliasByName: new Map(),
|
|
168
|
-
requiredExports: new Map(),
|
|
169
|
-
modelNameByIR: new Map(),
|
|
170
|
-
fileBySymbol: new Map(),
|
|
171
|
-
},
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const files = generateClient(overlaySpec, overlayCtx);
|
|
175
|
-
const workosFile = files.find((f) => f.path === 'src/workos.ts');
|
|
176
|
-
expect(workosFile).toBeDefined();
|
|
177
|
-
|
|
178
|
-
const content = workosFile!.content;
|
|
179
|
-
// Import path uses resolved name
|
|
180
|
-
expect(content).toContain("from './mfa/mfa'");
|
|
181
|
-
// Property uses resolved name
|
|
182
|
-
expect(content).toContain('readonly mfa = new Mfa(this);');
|
|
183
|
-
|
|
184
|
-
const barrel = files.find((f) => f.path === 'src/index.ts');
|
|
185
|
-
expect(barrel).toBeDefined();
|
|
186
|
-
// Barrel export uses resolved name for resource class
|
|
187
|
-
expect(barrel!.content).toContain("from './mfa/mfa'");
|
|
188
|
-
// Service barrel uses resolved directory name
|
|
189
|
-
expect(barrel!.content).toContain("export * from './mfa/interfaces'");
|
|
190
|
-
|
|
191
|
-
// Per-service barrel is generated with resolved directory
|
|
192
|
-
const serviceBarrel = files.find((f) => f.path === 'src/mfa/interfaces/index.ts');
|
|
193
|
-
expect(serviceBarrel).toBeDefined();
|
|
194
|
-
expect(serviceBarrel!.content).toContain("export * from './authentication-factor.interface';");
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('propagates @deprecated from baseline service class to the property declaration', () => {
|
|
198
|
-
// Regression test for PR #1535 reviewer comment r3075509969: when a
|
|
199
|
-
// service class has `@deprecated` in its JSDoc, TS's deprecation-lint
|
|
200
|
-
// only fires at `new X()` call sites — not at `workos.x` access unless
|
|
201
|
-
// the property itself is annotated. The emitter propagates the class
|
|
202
|
-
// deprecation to the property JSDoc so IDEs surface the strikethrough
|
|
203
|
-
// on every access.
|
|
204
|
-
const deprecatedCtx: EmitterContext = {
|
|
205
|
-
...ctx,
|
|
206
|
-
apiSurface: {
|
|
207
|
-
language: 'node',
|
|
208
|
-
extractedFrom: 'test',
|
|
209
|
-
extractedAt: '2026-01-01',
|
|
210
|
-
classes: {
|
|
211
|
-
Organizations: {
|
|
212
|
-
name: 'Organizations',
|
|
213
|
-
methods: {},
|
|
214
|
-
properties: {},
|
|
215
|
-
constructorParams: [{ name: 'workos', type: 'WorkOS', optional: false }],
|
|
216
|
-
deprecationMessage: 'Use `workos.connect` instead.',
|
|
25
|
+
const services: Service[] = [
|
|
26
|
+
{
|
|
27
|
+
name: 'Organizations',
|
|
28
|
+
operations: [
|
|
29
|
+
{
|
|
30
|
+
name: 'listOrganizations',
|
|
31
|
+
httpMethod: 'get',
|
|
32
|
+
path: '/organizations',
|
|
33
|
+
pathParams: [],
|
|
34
|
+
queryParams: [],
|
|
35
|
+
headerParams: [],
|
|
36
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
37
|
+
errors: [],
|
|
38
|
+
injectIdempotencyKey: false,
|
|
217
39
|
},
|
|
218
|
-
|
|
219
|
-
interfaces: {},
|
|
220
|
-
typeAliases: {},
|
|
221
|
-
enums: {},
|
|
222
|
-
exports: {},
|
|
40
|
+
],
|
|
223
41
|
},
|
|
224
|
-
|
|
42
|
+
];
|
|
225
43
|
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
const
|
|
44
|
+
const spec: ApiSpec = { ...emptySpec, services, models: [] };
|
|
45
|
+
const ctxWithServices: EmitterContext = { ...ctx, spec };
|
|
46
|
+
const result = generateClient(spec, ctxWithServices);
|
|
229
47
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
expect(content).
|
|
233
|
-
|
|
234
|
-
);
|
|
48
|
+
const workosFile = result.find((f) => f.path === 'src/workos.ts');
|
|
49
|
+
expect(workosFile).toBeDefined();
|
|
50
|
+
expect(workosFile!.content).toContain('export class WorkOS');
|
|
51
|
+
expect(workosFile!.content).toContain('readonly organizations = new Organizations(this)');
|
|
235
52
|
});
|
|
236
53
|
|
|
237
|
-
it('
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
54
|
+
it('generates barrel exports', () => {
|
|
55
|
+
const services: Service[] = [
|
|
56
|
+
{
|
|
57
|
+
name: 'Organizations',
|
|
58
|
+
operations: [
|
|
59
|
+
{
|
|
60
|
+
name: 'listOrganizations',
|
|
61
|
+
httpMethod: 'get',
|
|
62
|
+
path: '/organizations',
|
|
63
|
+
pathParams: [],
|
|
64
|
+
queryParams: [],
|
|
65
|
+
headerParams: [],
|
|
66
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
67
|
+
errors: [],
|
|
68
|
+
injectIdempotencyKey: false,
|
|
251
69
|
},
|
|
252
|
-
|
|
253
|
-
interfaces: {},
|
|
254
|
-
typeAliases: {},
|
|
255
|
-
enums: {},
|
|
256
|
-
exports: {},
|
|
257
|
-
},
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
const files = generateClient(spec, deprecatedCtx);
|
|
261
|
-
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
|
262
|
-
expect(workosFile.content).toMatch(/\/\*\* @deprecated \*\/\s+readonly organizations = new Organizations\(this\);/);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it('does not emit @deprecated when the baseline class has no deprecationMessage', () => {
|
|
266
|
-
const files = generateClient(spec, ctx);
|
|
267
|
-
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
|
268
|
-
expect(workosFile.content).not.toContain('@deprecated');
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it('does not generate error handling in WorkOS client (lives in WorkOSBase)', () => {
|
|
272
|
-
const files = generateClient(spec, ctx);
|
|
273
|
-
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
|
274
|
-
const content = workosFile.content;
|
|
275
|
-
|
|
276
|
-
expect(content).not.toContain('handleHttpError');
|
|
277
|
-
expect(content).not.toContain('UnauthorizedException');
|
|
278
|
-
expect(content).not.toContain('NotFoundException');
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('skips explicit model export when name is already in apiSurface.exports', () => {
|
|
282
|
-
// Simulates the Event shadowing bug: the existing SDK already exports "Event"
|
|
283
|
-
// via a wildcard re-export (e.g., a hand-written 60+ member discriminated union).
|
|
284
|
-
// The barrel must not emit an explicit `export type { Event }` that would shadow it.
|
|
285
|
-
const eventService: Service = {
|
|
286
|
-
name: 'Events',
|
|
287
|
-
operations: [
|
|
288
|
-
{
|
|
289
|
-
name: 'listEvents',
|
|
290
|
-
httpMethod: 'get',
|
|
291
|
-
path: '/events',
|
|
292
|
-
pathParams: [],
|
|
293
|
-
queryParams: [],
|
|
294
|
-
headerParams: [],
|
|
295
|
-
response: { kind: 'model', name: 'Event' },
|
|
296
|
-
errors: [],
|
|
297
|
-
injectIdempotencyKey: false,
|
|
298
|
-
},
|
|
299
|
-
],
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
const eventModel: Model = {
|
|
303
|
-
name: 'Event',
|
|
304
|
-
fields: [
|
|
305
|
-
{
|
|
306
|
-
name: 'id',
|
|
307
|
-
type: { kind: 'primitive', type: 'string' },
|
|
308
|
-
required: true,
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
name: 'event',
|
|
312
|
-
type: { kind: 'primitive', type: 'string' },
|
|
313
|
-
required: true,
|
|
314
|
-
},
|
|
315
|
-
],
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
const otherModel: Model = {
|
|
319
|
-
name: 'EventCursor',
|
|
320
|
-
fields: [
|
|
321
|
-
{
|
|
322
|
-
name: 'cursor',
|
|
323
|
-
type: { kind: 'primitive', type: 'string' },
|
|
324
|
-
required: true,
|
|
325
|
-
},
|
|
326
|
-
],
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
const eventSpec: ApiSpec = {
|
|
330
|
-
name: 'Test',
|
|
331
|
-
version: '1.0.0',
|
|
332
|
-
baseUrl: 'https://api.example.com',
|
|
333
|
-
services: [eventService],
|
|
334
|
-
models: [eventModel, otherModel],
|
|
335
|
-
enums: [],
|
|
336
|
-
sdk: defaultSdkBehavior(),
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
const surface: ApiSurface = {
|
|
340
|
-
language: 'node',
|
|
341
|
-
extractedFrom: '/tmp/test-sdk',
|
|
342
|
-
extractedAt: '2025-01-01T00:00:00Z',
|
|
343
|
-
classes: {},
|
|
344
|
-
interfaces: {
|
|
345
|
-
Event: {
|
|
346
|
-
name: 'Event',
|
|
347
|
-
sourceFile: 'src/common/interfaces/event.interface.ts',
|
|
348
|
-
fields: {},
|
|
349
|
-
extends: [],
|
|
350
|
-
},
|
|
351
|
-
},
|
|
352
|
-
typeAliases: {},
|
|
353
|
-
enums: {},
|
|
354
|
-
// The existing SDK's barrel re-exports "Event" via a wildcard chain
|
|
355
|
-
exports: {
|
|
356
|
-
'src/common/interfaces/event.interface.ts': ['Event'],
|
|
357
|
-
'src/index.ts': ['Event', 'WorkOS', 'Events'],
|
|
358
|
-
},
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
const eventCtx: EmitterContext = {
|
|
362
|
-
namespace: 'workos',
|
|
363
|
-
namespacePascal: 'WorkOS',
|
|
364
|
-
spec: eventSpec,
|
|
365
|
-
apiSurface: surface,
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
const files = generateClient(eventSpec, eventCtx);
|
|
369
|
-
const barrel = files.find((f) => f.path === 'src/index.ts')!;
|
|
370
|
-
const content = barrel.content;
|
|
371
|
-
|
|
372
|
-
// Event must NOT appear as an explicit named export — it would shadow the wildcard
|
|
373
|
-
expect(content).not.toContain('export type { Event,');
|
|
374
|
-
expect(content).not.toContain('export type { Event }');
|
|
375
|
-
|
|
376
|
-
// EventCursor is unreachable (not referenced by any service), so it should
|
|
377
|
-
// NOT be exported — oagen only generates interface files for reachable models
|
|
378
|
-
expect(content).not.toContain("export * from './common/interfaces'");
|
|
379
|
-
expect(content).not.toContain('EventCursor');
|
|
380
|
-
|
|
381
|
-
// The resource class export should still be present
|
|
382
|
-
expect(content).toContain("export { Events } from './events/events'");
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it('skips explicit enum export when name is already in apiSurface.exports', () => {
|
|
386
|
-
const enumSpec: ApiSpec = {
|
|
387
|
-
name: 'Test',
|
|
388
|
-
version: '1.0.0',
|
|
389
|
-
baseUrl: 'https://api.example.com',
|
|
390
|
-
services: [service],
|
|
391
|
-
models: [model],
|
|
392
|
-
enums: [
|
|
393
|
-
{
|
|
394
|
-
name: 'EventType',
|
|
395
|
-
values: [
|
|
396
|
-
{ name: 'CONNECTION_ACTIVATED', value: 'connection.activated' },
|
|
397
|
-
{ name: 'CONNECTION_DELETED', value: 'connection.deleted' },
|
|
398
|
-
],
|
|
399
|
-
},
|
|
400
|
-
],
|
|
401
|
-
sdk: defaultSdkBehavior(),
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
const surface: ApiSurface = {
|
|
405
|
-
language: 'node',
|
|
406
|
-
extractedFrom: '/tmp/test-sdk',
|
|
407
|
-
extractedAt: '2025-01-01T00:00:00Z',
|
|
408
|
-
classes: {},
|
|
409
|
-
interfaces: {},
|
|
410
|
-
typeAliases: {},
|
|
411
|
-
enums: {},
|
|
412
|
-
exports: {
|
|
413
|
-
'src/common/interfaces/event-type.interface.ts': ['EventType'],
|
|
70
|
+
],
|
|
414
71
|
},
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const enumCtx: EmitterContext = {
|
|
418
|
-
namespace: 'workos',
|
|
419
|
-
namespacePascal: 'WorkOS',
|
|
420
|
-
spec: enumSpec,
|
|
421
|
-
apiSurface: surface,
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
const files = generateClient(enumSpec, enumCtx);
|
|
425
|
-
const barrel = files.find((f) => f.path === 'src/index.ts')!;
|
|
426
|
-
|
|
427
|
-
// EventType should NOT appear as an explicit export — already covered by wildcard
|
|
428
|
-
expect(barrel.content).not.toContain('export type { EventType }');
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it('emits model exports normally when no apiSurface is present', () => {
|
|
432
|
-
// Without apiSurface, all models should be exported via service barrel
|
|
433
|
-
const files = generateClient(spec, ctx);
|
|
434
|
-
const barrel = files.find((f) => f.path === 'src/index.ts')!;
|
|
435
|
-
expect(barrel.content).toContain("export * from './organizations/interfaces'");
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it('renders spec.description as JSDoc on WorkOS class', () => {
|
|
439
|
-
const specWithDesc: ApiSpec = {
|
|
440
|
-
...spec,
|
|
441
|
-
description: 'The WorkOS API provides a unified interface for enterprise features.',
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
const descCtx: EmitterContext = {
|
|
445
|
-
namespace: 'workos',
|
|
446
|
-
namespacePascal: 'WorkOS',
|
|
447
|
-
spec: specWithDesc,
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
const files = generateClient(specWithDesc, descCtx);
|
|
451
|
-
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
|
452
|
-
const content = workosFile.content;
|
|
453
|
-
|
|
454
|
-
expect(content).toContain('/** The WorkOS API provides a unified interface for enterprise features. */');
|
|
455
|
-
expect(content).toContain('export class WorkOS extends WorkOSBase {');
|
|
456
|
-
});
|
|
72
|
+
];
|
|
457
73
|
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
values: [
|
|
462
|
-
{ name: 'ADFSSAML', value: 'ADFSSAML' },
|
|
463
|
-
{ name: 'GoogleOAuth', value: 'GoogleOAuth' },
|
|
464
|
-
],
|
|
465
|
-
};
|
|
466
|
-
const aliasEnumDef: Enum = {
|
|
467
|
-
name: 'DirectoryState',
|
|
468
|
-
values: [
|
|
469
|
-
{ name: 'active', value: 'active' },
|
|
470
|
-
{ name: 'inactive', value: 'inactive' },
|
|
471
|
-
],
|
|
472
|
-
};
|
|
473
|
-
const enumService: Service = {
|
|
474
|
-
name: 'Payments',
|
|
475
|
-
operations: [
|
|
476
|
-
{
|
|
477
|
-
name: 'listPayments',
|
|
478
|
-
httpMethod: 'get',
|
|
479
|
-
path: '/payments',
|
|
480
|
-
pathParams: [],
|
|
481
|
-
queryParams: [
|
|
482
|
-
{
|
|
483
|
-
name: 'type',
|
|
484
|
-
type: {
|
|
485
|
-
kind: 'enum',
|
|
486
|
-
name: 'ConnectionType',
|
|
487
|
-
values: ['ADFSSAML', 'GoogleOAuth'],
|
|
488
|
-
},
|
|
489
|
-
required: false,
|
|
490
|
-
},
|
|
491
|
-
],
|
|
492
|
-
headerParams: [],
|
|
493
|
-
response: { kind: 'model', name: 'Organization' },
|
|
494
|
-
errors: [],
|
|
495
|
-
injectIdempotencyKey: false,
|
|
496
|
-
},
|
|
497
|
-
],
|
|
498
|
-
};
|
|
499
|
-
const dirService: Service = {
|
|
500
|
-
name: 'Invoices',
|
|
501
|
-
operations: [
|
|
502
|
-
{
|
|
503
|
-
name: 'listInvoices',
|
|
504
|
-
httpMethod: 'get',
|
|
505
|
-
path: '/invoices',
|
|
506
|
-
pathParams: [],
|
|
507
|
-
queryParams: [
|
|
508
|
-
{
|
|
509
|
-
name: 'state',
|
|
510
|
-
type: {
|
|
511
|
-
kind: 'enum',
|
|
512
|
-
name: 'DirectoryState',
|
|
513
|
-
values: ['active', 'inactive'],
|
|
514
|
-
},
|
|
515
|
-
required: false,
|
|
516
|
-
},
|
|
517
|
-
],
|
|
518
|
-
headerParams: [],
|
|
519
|
-
response: { kind: 'model', name: 'Organization' },
|
|
520
|
-
errors: [],
|
|
521
|
-
injectIdempotencyKey: false,
|
|
522
|
-
},
|
|
523
|
-
],
|
|
524
|
-
};
|
|
525
|
-
const enumSpec: ApiSpec = {
|
|
526
|
-
name: 'Test',
|
|
527
|
-
version: '1.0.0',
|
|
528
|
-
baseUrl: 'https://api.example.com',
|
|
529
|
-
services: [service, enumService, dirService],
|
|
530
|
-
models: [model],
|
|
531
|
-
enums: [enumDef, aliasEnumDef],
|
|
532
|
-
sdk: defaultSdkBehavior(),
|
|
533
|
-
};
|
|
534
|
-
const enumCtx: EmitterContext = {
|
|
535
|
-
namespace: 'workos',
|
|
536
|
-
namespacePascal: 'WorkOS',
|
|
537
|
-
spec: enumSpec,
|
|
538
|
-
apiSurface: {
|
|
539
|
-
language: 'node',
|
|
540
|
-
extractedFrom: 'test',
|
|
541
|
-
extractedAt: '2024-01-01',
|
|
542
|
-
interfaces: {},
|
|
543
|
-
classes: {},
|
|
544
|
-
enums: {
|
|
545
|
-
ConnectionType: {
|
|
546
|
-
name: 'ConnectionType',
|
|
547
|
-
members: { ADFSSAML: 'ADFSSAML', GoogleOAuth: 'GoogleOAuth' },
|
|
548
|
-
},
|
|
549
|
-
},
|
|
550
|
-
typeAliases: {},
|
|
551
|
-
exports: {},
|
|
552
|
-
},
|
|
553
|
-
};
|
|
74
|
+
const spec: ApiSpec = { ...emptySpec, services, models: [] };
|
|
75
|
+
const ctxWithServices: EmitterContext = { ...ctx, spec };
|
|
76
|
+
const result = generateClient(spec, ctxWithServices);
|
|
554
77
|
|
|
555
|
-
const
|
|
556
|
-
const barrel = files.find((f) => f.path === 'src/index.ts');
|
|
78
|
+
const barrel = result.find((f) => f.path === 'src/index.ts');
|
|
557
79
|
expect(barrel).toBeDefined();
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
headerParams: [],
|
|
579
|
-
response: { kind: 'model', name: 'ConnectionList' },
|
|
580
|
-
errors: [],
|
|
581
|
-
injectIdempotencyKey: false,
|
|
582
|
-
},
|
|
583
|
-
{
|
|
584
|
-
name: 'getConnection',
|
|
585
|
-
httpMethod: 'get',
|
|
586
|
-
path: '/payments/{id}',
|
|
587
|
-
pathParams: [
|
|
588
|
-
{
|
|
589
|
-
name: 'id',
|
|
590
|
-
type: { kind: 'primitive', type: 'string' },
|
|
591
|
-
required: true,
|
|
592
|
-
},
|
|
593
|
-
],
|
|
594
|
-
queryParams: [],
|
|
595
|
-
headerParams: [],
|
|
596
|
-
response: { kind: 'model', name: 'Connection' },
|
|
597
|
-
errors: [],
|
|
598
|
-
injectIdempotencyKey: false,
|
|
599
|
-
},
|
|
600
|
-
],
|
|
601
|
-
};
|
|
602
|
-
|
|
603
|
-
const connectionModel: Model = {
|
|
604
|
-
name: 'Connection',
|
|
605
|
-
fields: [
|
|
606
|
-
{
|
|
607
|
-
name: 'id',
|
|
608
|
-
type: { kind: 'primitive', type: 'string' },
|
|
609
|
-
required: true,
|
|
610
|
-
},
|
|
611
|
-
{
|
|
612
|
-
name: 'name',
|
|
613
|
-
type: { kind: 'primitive', type: 'string' },
|
|
614
|
-
required: true,
|
|
615
|
-
},
|
|
616
|
-
],
|
|
617
|
-
};
|
|
618
|
-
|
|
619
|
-
const radarService: Service = {
|
|
620
|
-
name: 'Radar',
|
|
621
|
-
operations: [
|
|
622
|
-
{
|
|
623
|
-
name: 'assess',
|
|
624
|
-
httpMethod: 'post',
|
|
625
|
-
path: '/radar/assess',
|
|
626
|
-
pathParams: [],
|
|
627
|
-
queryParams: [],
|
|
628
|
-
headerParams: [],
|
|
629
|
-
response: { kind: 'model', name: 'RadarResult' },
|
|
630
|
-
errors: [],
|
|
631
|
-
injectIdempotencyKey: false,
|
|
632
|
-
},
|
|
633
|
-
],
|
|
634
|
-
};
|
|
635
|
-
|
|
636
|
-
const radarModel: Model = {
|
|
637
|
-
name: 'RadarResult',
|
|
638
|
-
fields: [
|
|
639
|
-
{
|
|
640
|
-
name: 'score',
|
|
641
|
-
type: { kind: 'primitive', type: 'number' },
|
|
642
|
-
required: true,
|
|
643
|
-
},
|
|
644
|
-
],
|
|
645
|
-
};
|
|
646
|
-
|
|
647
|
-
const coveredSpec: ApiSpec = {
|
|
648
|
-
name: 'Test',
|
|
649
|
-
version: '1.0.0',
|
|
650
|
-
baseUrl: 'https://api.example.com',
|
|
651
|
-
services: [connectionsService, radarService],
|
|
652
|
-
models: [connectionModel, radarModel],
|
|
653
|
-
enums: [],
|
|
654
|
-
sdk: defaultSdkBehavior(),
|
|
655
|
-
};
|
|
656
|
-
|
|
657
|
-
const coveredCtx: EmitterContext = {
|
|
658
|
-
namespace: 'workos',
|
|
659
|
-
namespacePascal: 'WorkOS',
|
|
660
|
-
spec: coveredSpec,
|
|
661
|
-
apiSurface: {
|
|
662
|
-
language: 'node',
|
|
663
|
-
extractedFrom: 'test',
|
|
664
|
-
extractedAt: '2024-01-01',
|
|
665
|
-
interfaces: {},
|
|
666
|
-
classes: {
|
|
667
|
-
Sso: {
|
|
668
|
-
name: 'Sso',
|
|
669
|
-
methods: {
|
|
670
|
-
listConnections: [
|
|
671
|
-
{
|
|
672
|
-
name: 'listConnections',
|
|
673
|
-
params: [],
|
|
674
|
-
returnType: 'Promise<AutoPaginatable<Connection>>',
|
|
675
|
-
async: true,
|
|
676
|
-
},
|
|
677
|
-
],
|
|
678
|
-
getConnection: [
|
|
679
|
-
{
|
|
680
|
-
name: 'getConnection',
|
|
681
|
-
params: [{ name: 'id', type: 'string', optional: false }],
|
|
682
|
-
returnType: 'Promise<Connection>',
|
|
683
|
-
async: true,
|
|
684
|
-
},
|
|
685
|
-
],
|
|
686
|
-
},
|
|
687
|
-
properties: {},
|
|
688
|
-
constructorParams: [],
|
|
689
|
-
},
|
|
690
|
-
},
|
|
691
|
-
enums: {},
|
|
692
|
-
typeAliases: {},
|
|
693
|
-
exports: {},
|
|
694
|
-
},
|
|
695
|
-
overlayLookup: {
|
|
696
|
-
methodByOperation: new Map([
|
|
697
|
-
[
|
|
698
|
-
'GET /connections',
|
|
699
|
-
{
|
|
700
|
-
className: 'Sso',
|
|
701
|
-
methodName: 'listConnections',
|
|
702
|
-
params: [],
|
|
703
|
-
returnType: 'Promise<AutoPaginatable<Connection>>',
|
|
704
|
-
},
|
|
705
|
-
],
|
|
706
|
-
[
|
|
707
|
-
'GET /connections/{id}',
|
|
708
|
-
{
|
|
709
|
-
className: 'Sso',
|
|
710
|
-
methodName: 'getConnection',
|
|
711
|
-
params: [{ name: 'id', type: 'string', optional: false }],
|
|
712
|
-
returnType: 'Promise<Connection>',
|
|
713
|
-
},
|
|
714
|
-
],
|
|
715
|
-
]),
|
|
716
|
-
httpKeyByMethod: new Map(),
|
|
717
|
-
interfaceByName: new Map(),
|
|
718
|
-
typeAliasByName: new Map(),
|
|
719
|
-
requiredExports: new Map(),
|
|
720
|
-
modelNameByIR: new Map(),
|
|
721
|
-
fileBySymbol: new Map(),
|
|
722
|
-
},
|
|
723
|
-
};
|
|
724
|
-
|
|
725
|
-
const files = generateClient(coveredSpec, coveredCtx);
|
|
726
|
-
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
|
727
|
-
const content = workosFile.content;
|
|
728
|
-
|
|
729
|
-
// Connections service should NOT appear (fully covered by Sso in baseline)
|
|
730
|
-
expect(content).not.toContain('Connections');
|
|
731
|
-
expect(content).not.toContain("from './sso/sso'");
|
|
732
|
-
|
|
733
|
-
// Radar service should still appear (not covered)
|
|
734
|
-
expect(content).toContain('readonly radar = new Radar(this);');
|
|
735
|
-
expect(content).toContain("import { Radar } from './radar/radar';");
|
|
736
|
-
|
|
737
|
-
// Barrel should also skip the Connections resource class export
|
|
738
|
-
const barrel = files.find((f) => f.path === 'src/index.ts')!;
|
|
739
|
-
const barrelContent = barrel.content;
|
|
740
|
-
expect(barrelContent).not.toContain('export { Sso }');
|
|
741
|
-
expect(barrelContent).not.toContain('export { Connections }');
|
|
742
|
-
|
|
743
|
-
// Covered services don't generate barrel exports — their types are
|
|
744
|
-
// already exported by the hand-written service's own barrel.
|
|
745
|
-
expect(barrelContent).not.toContain("export * from './sso/interfaces'");
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
it('does not skip services when only some operations are covered', () => {
|
|
749
|
-
const partialService: Service = {
|
|
750
|
-
name: 'Invoices',
|
|
751
|
-
operations: [
|
|
752
|
-
{
|
|
753
|
-
name: 'listInvoices',
|
|
754
|
-
httpMethod: 'get',
|
|
755
|
-
path: '/invoices',
|
|
756
|
-
pathParams: [],
|
|
757
|
-
queryParams: [],
|
|
758
|
-
headerParams: [],
|
|
759
|
-
response: { kind: 'model', name: 'DirectoryList' },
|
|
760
|
-
errors: [],
|
|
761
|
-
injectIdempotencyKey: false,
|
|
762
|
-
},
|
|
763
|
-
{
|
|
764
|
-
name: 'createInvoice',
|
|
765
|
-
httpMethod: 'post',
|
|
766
|
-
path: '/invoices',
|
|
767
|
-
pathParams: [],
|
|
768
|
-
queryParams: [],
|
|
769
|
-
headerParams: [],
|
|
770
|
-
response: { kind: 'model', name: 'Invoice' },
|
|
771
|
-
errors: [],
|
|
772
|
-
injectIdempotencyKey: false,
|
|
773
|
-
},
|
|
774
|
-
],
|
|
775
|
-
};
|
|
776
|
-
|
|
777
|
-
const dirModel: Model = {
|
|
778
|
-
name: 'Invoice',
|
|
779
|
-
fields: [
|
|
780
|
-
{
|
|
781
|
-
name: 'id',
|
|
782
|
-
type: { kind: 'primitive', type: 'string' },
|
|
783
|
-
required: true,
|
|
784
|
-
},
|
|
785
|
-
],
|
|
786
|
-
};
|
|
787
|
-
|
|
788
|
-
const partialSpec: ApiSpec = {
|
|
789
|
-
name: 'Test',
|
|
790
|
-
version: '1.0.0',
|
|
791
|
-
baseUrl: 'https://api.example.com',
|
|
792
|
-
services: [partialService],
|
|
793
|
-
models: [dirModel],
|
|
794
|
-
enums: [],
|
|
795
|
-
sdk: defaultSdkBehavior(),
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
const partialCtx: EmitterContext = {
|
|
799
|
-
namespace: 'workos',
|
|
800
|
-
namespacePascal: 'WorkOS',
|
|
801
|
-
spec: partialSpec,
|
|
802
|
-
apiSurface: {
|
|
803
|
-
language: 'node',
|
|
804
|
-
extractedFrom: 'test',
|
|
805
|
-
extractedAt: '2024-01-01',
|
|
806
|
-
interfaces: {},
|
|
807
|
-
classes: {
|
|
808
|
-
Billing: {
|
|
809
|
-
name: 'Billing',
|
|
810
|
-
methods: {
|
|
811
|
-
listInvoices: [
|
|
812
|
-
{
|
|
813
|
-
name: 'listInvoices',
|
|
814
|
-
params: [],
|
|
815
|
-
returnType: 'Promise<AutoPaginatable<Invoice>>',
|
|
816
|
-
async: true,
|
|
817
|
-
},
|
|
818
|
-
],
|
|
819
|
-
},
|
|
820
|
-
properties: {},
|
|
821
|
-
constructorParams: [],
|
|
80
|
+
expect(barrel!.content).toContain("export * from './common/exceptions';");
|
|
81
|
+
expect(barrel!.content).toContain("export { AutoPaginatable } from './common/utils/pagination';");
|
|
82
|
+
expect(barrel!.content).toContain("export { WorkOS } from './workos';");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('does not generate package.json or tsconfig.json', () => {
|
|
86
|
+
const services: Service[] = [
|
|
87
|
+
{
|
|
88
|
+
name: 'Organizations',
|
|
89
|
+
operations: [
|
|
90
|
+
{
|
|
91
|
+
name: 'listOrganizations',
|
|
92
|
+
httpMethod: 'get',
|
|
93
|
+
path: '/organizations',
|
|
94
|
+
pathParams: [],
|
|
95
|
+
queryParams: [],
|
|
96
|
+
headerParams: [],
|
|
97
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
98
|
+
errors: [],
|
|
99
|
+
injectIdempotencyKey: false,
|
|
822
100
|
},
|
|
823
|
-
|
|
824
|
-
enums: {},
|
|
825
|
-
typeAliases: {},
|
|
826
|
-
exports: {},
|
|
827
|
-
},
|
|
828
|
-
overlayLookup: {
|
|
829
|
-
methodByOperation: new Map([
|
|
830
|
-
[
|
|
831
|
-
'GET /invoices',
|
|
832
|
-
{
|
|
833
|
-
className: 'Billing',
|
|
834
|
-
methodName: 'listInvoices',
|
|
835
|
-
params: [],
|
|
836
|
-
returnType: 'Promise<AutoPaginatable<Invoice>>',
|
|
837
|
-
},
|
|
838
|
-
],
|
|
839
|
-
]),
|
|
840
|
-
httpKeyByMethod: new Map(),
|
|
841
|
-
interfaceByName: new Map(),
|
|
842
|
-
typeAliasByName: new Map(),
|
|
843
|
-
requiredExports: new Map(),
|
|
844
|
-
modelNameByIR: new Map(),
|
|
845
|
-
fileBySymbol: new Map(),
|
|
101
|
+
],
|
|
846
102
|
},
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
const files = generateClient(partialSpec, partialCtx);
|
|
850
|
-
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
|
851
|
-
const content = workosFile.content;
|
|
852
|
-
|
|
853
|
-
// Service should still be generated because it has an uncovered operation
|
|
854
|
-
expect(content).toContain('Billing');
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
it('does not skip services when no overlay is provided', () => {
|
|
858
|
-
const files = generateClient(spec, ctx);
|
|
859
|
-
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
|
860
|
-
expect(workosFile.content).toContain('readonly organizations = new Organizations(this);');
|
|
861
|
-
});
|
|
862
|
-
|
|
863
|
-
it('does not skip services when overlay exists but no apiSurface baseline', () => {
|
|
864
|
-
const mfaService: Service = {
|
|
865
|
-
name: 'Analytics',
|
|
866
|
-
operations: [
|
|
867
|
-
{
|
|
868
|
-
name: 'enrollFactor',
|
|
869
|
-
httpMethod: 'post',
|
|
870
|
-
path: '/auth/factors/enroll',
|
|
871
|
-
pathParams: [],
|
|
872
|
-
queryParams: [],
|
|
873
|
-
headerParams: [],
|
|
874
|
-
response: { kind: 'model', name: 'AuthenticationFactor' },
|
|
875
|
-
errors: [],
|
|
876
|
-
injectIdempotencyKey: true,
|
|
877
|
-
},
|
|
878
|
-
],
|
|
879
|
-
};
|
|
880
|
-
|
|
881
|
-
const mfaModel: Model = {
|
|
882
|
-
name: 'AuthenticationFactor',
|
|
883
|
-
fields: [
|
|
884
|
-
{
|
|
885
|
-
name: 'id',
|
|
886
|
-
type: { kind: 'primitive', type: 'string' },
|
|
887
|
-
required: true,
|
|
888
|
-
},
|
|
889
|
-
],
|
|
890
|
-
};
|
|
103
|
+
];
|
|
891
104
|
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
baseUrl: 'https://api.example.com',
|
|
896
|
-
services: [mfaService],
|
|
897
|
-
models: [mfaModel],
|
|
898
|
-
enums: [],
|
|
899
|
-
sdk: defaultSdkBehavior(),
|
|
900
|
-
};
|
|
901
|
-
|
|
902
|
-
const namingOnlyCtx: EmitterContext = {
|
|
903
|
-
namespace: 'workos',
|
|
904
|
-
namespacePascal: 'WorkOS',
|
|
905
|
-
spec: mfaSpec,
|
|
906
|
-
overlayLookup: {
|
|
907
|
-
methodByOperation: new Map([
|
|
908
|
-
[
|
|
909
|
-
'POST /auth/factors/enroll',
|
|
910
|
-
{
|
|
911
|
-
className: 'Analytics',
|
|
912
|
-
methodName: 'enrollFactor',
|
|
913
|
-
params: [],
|
|
914
|
-
returnType: 'void',
|
|
915
|
-
},
|
|
916
|
-
],
|
|
917
|
-
]),
|
|
918
|
-
httpKeyByMethod: new Map(),
|
|
919
|
-
interfaceByName: new Map(),
|
|
920
|
-
typeAliasByName: new Map(),
|
|
921
|
-
requiredExports: new Map(),
|
|
922
|
-
modelNameByIR: new Map(),
|
|
923
|
-
fileBySymbol: new Map(),
|
|
924
|
-
},
|
|
925
|
-
};
|
|
105
|
+
const spec: ApiSpec = { ...emptySpec, services, models: [] };
|
|
106
|
+
const ctxWithServices: EmitterContext = { ...ctx, spec };
|
|
107
|
+
const result = generateClient(spec, ctxWithServices);
|
|
926
108
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
expect(workosFile.content).toContain('readonly analytics = new Analytics(this);');
|
|
109
|
+
expect(result.every((f) => !f.path.includes('package.json'))).toBe(true);
|
|
110
|
+
expect(result.every((f) => !f.path.includes('tsconfig.json'))).toBe(true);
|
|
930
111
|
});
|
|
931
112
|
});
|
|
932
113
|
|
|
933
114
|
describe('isServiceCoveredByExisting', () => {
|
|
934
|
-
const emptySpec: ApiSpec = {
|
|
935
|
-
name: 'Test',
|
|
936
|
-
version: '1.0.0',
|
|
937
|
-
baseUrl: '',
|
|
938
|
-
services: [],
|
|
939
|
-
models: [],
|
|
940
|
-
enums: [],
|
|
941
|
-
sdk: defaultSdkBehavior(),
|
|
942
|
-
};
|
|
943
|
-
|
|
944
115
|
it('returns false when no overlay is provided', () => {
|
|
945
|
-
const
|
|
946
|
-
name: '
|
|
116
|
+
const service: Service = {
|
|
117
|
+
name: 'Organizations',
|
|
947
118
|
operations: [
|
|
948
119
|
{
|
|
949
|
-
name: '
|
|
120
|
+
name: 'listOrganizations',
|
|
950
121
|
httpMethod: 'get',
|
|
951
|
-
path: '/
|
|
122
|
+
path: '/organizations',
|
|
952
123
|
pathParams: [],
|
|
953
124
|
queryParams: [],
|
|
954
125
|
headerParams: [],
|
|
955
|
-
response: { kind: '
|
|
126
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
956
127
|
errors: [],
|
|
957
128
|
injectIdempotencyKey: false,
|
|
958
129
|
},
|
|
959
130
|
],
|
|
960
131
|
};
|
|
961
|
-
|
|
962
|
-
namespace: 'workos',
|
|
963
|
-
namespacePascal: 'WorkOS',
|
|
964
|
-
spec: emptySpec,
|
|
965
|
-
};
|
|
966
|
-
expect(isServiceCoveredByExisting(svc, noOverlayCtx)).toBe(false);
|
|
132
|
+
expect(isServiceCoveredByExisting(service, ctx)).toBe(false);
|
|
967
133
|
});
|
|
968
134
|
|
|
969
135
|
it('returns false when overlay is empty', () => {
|
|
970
|
-
const
|
|
971
|
-
name: '
|
|
136
|
+
const service: Service = {
|
|
137
|
+
name: 'Organizations',
|
|
972
138
|
operations: [
|
|
973
139
|
{
|
|
974
|
-
name: '
|
|
140
|
+
name: 'listOrganizations',
|
|
975
141
|
httpMethod: 'get',
|
|
976
|
-
path: '/
|
|
142
|
+
path: '/organizations',
|
|
977
143
|
pathParams: [],
|
|
978
144
|
queryParams: [],
|
|
979
145
|
headerParams: [],
|
|
980
|
-
response: { kind: '
|
|
146
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
981
147
|
errors: [],
|
|
982
148
|
injectIdempotencyKey: false,
|
|
983
149
|
},
|
|
984
150
|
],
|
|
985
151
|
};
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
namespacePascal: 'WorkOS',
|
|
989
|
-
spec: emptySpec,
|
|
152
|
+
const ctxWithOverlay: EmitterContext = {
|
|
153
|
+
...ctx,
|
|
990
154
|
overlayLookup: {
|
|
991
155
|
methodByOperation: new Map(),
|
|
992
156
|
httpKeyByMethod: new Map(),
|
|
@@ -997,301 +161,42 @@ describe('isServiceCoveredByExisting', () => {
|
|
|
997
161
|
fileBySymbol: new Map(),
|
|
998
162
|
},
|
|
999
163
|
};
|
|
1000
|
-
expect(isServiceCoveredByExisting(
|
|
1001
|
-
});
|
|
1002
|
-
|
|
1003
|
-
it('returns true when all operations are covered by overlay and class exists in baseline', () => {
|
|
1004
|
-
const svc: Service = {
|
|
1005
|
-
name: 'Connections',
|
|
1006
|
-
operations: [
|
|
1007
|
-
{
|
|
1008
|
-
name: 'listConnections',
|
|
1009
|
-
httpMethod: 'get',
|
|
1010
|
-
path: '/connections',
|
|
1011
|
-
pathParams: [],
|
|
1012
|
-
queryParams: [],
|
|
1013
|
-
headerParams: [],
|
|
1014
|
-
response: { kind: 'model', name: 'ConnectionList' },
|
|
1015
|
-
errors: [],
|
|
1016
|
-
injectIdempotencyKey: false,
|
|
1017
|
-
},
|
|
1018
|
-
{
|
|
1019
|
-
name: 'getConnection',
|
|
1020
|
-
httpMethod: 'get',
|
|
1021
|
-
path: '/connections/{id}',
|
|
1022
|
-
pathParams: [
|
|
1023
|
-
{
|
|
1024
|
-
name: 'id',
|
|
1025
|
-
type: { kind: 'primitive', type: 'string' },
|
|
1026
|
-
required: true,
|
|
1027
|
-
},
|
|
1028
|
-
],
|
|
1029
|
-
queryParams: [],
|
|
1030
|
-
headerParams: [],
|
|
1031
|
-
response: { kind: 'model', name: 'Connection' },
|
|
1032
|
-
errors: [],
|
|
1033
|
-
injectIdempotencyKey: false,
|
|
1034
|
-
},
|
|
1035
|
-
],
|
|
1036
|
-
};
|
|
1037
|
-
const fullCoverageCtx: EmitterContext = {
|
|
1038
|
-
namespace: 'workos',
|
|
1039
|
-
namespacePascal: 'WorkOS',
|
|
1040
|
-
spec: emptySpec,
|
|
1041
|
-
apiSurface: {
|
|
1042
|
-
language: 'node',
|
|
1043
|
-
extractedFrom: 'test',
|
|
1044
|
-
extractedAt: '2024-01-01',
|
|
1045
|
-
interfaces: {},
|
|
1046
|
-
classes: {
|
|
1047
|
-
Sso: {
|
|
1048
|
-
name: 'Sso',
|
|
1049
|
-
methods: {},
|
|
1050
|
-
properties: {},
|
|
1051
|
-
constructorParams: [],
|
|
1052
|
-
},
|
|
1053
|
-
},
|
|
1054
|
-
enums: {},
|
|
1055
|
-
typeAliases: {},
|
|
1056
|
-
exports: {},
|
|
1057
|
-
},
|
|
1058
|
-
overlayLookup: {
|
|
1059
|
-
methodByOperation: new Map([
|
|
1060
|
-
[
|
|
1061
|
-
'GET /connections',
|
|
1062
|
-
{
|
|
1063
|
-
className: 'Sso',
|
|
1064
|
-
methodName: 'listConnections',
|
|
1065
|
-
params: [],
|
|
1066
|
-
returnType: 'Promise<AutoPaginatable<Connection>>',
|
|
1067
|
-
},
|
|
1068
|
-
],
|
|
1069
|
-
[
|
|
1070
|
-
'GET /connections/{id}',
|
|
1071
|
-
{
|
|
1072
|
-
className: 'Sso',
|
|
1073
|
-
methodName: 'getConnection',
|
|
1074
|
-
params: [{ name: 'id', type: 'string', optional: false }],
|
|
1075
|
-
returnType: 'Promise<Connection>',
|
|
1076
|
-
},
|
|
1077
|
-
],
|
|
1078
|
-
]),
|
|
1079
|
-
httpKeyByMethod: new Map(),
|
|
1080
|
-
interfaceByName: new Map(),
|
|
1081
|
-
typeAliasByName: new Map(),
|
|
1082
|
-
requiredExports: new Map(),
|
|
1083
|
-
modelNameByIR: new Map(),
|
|
1084
|
-
fileBySymbol: new Map(),
|
|
1085
|
-
},
|
|
1086
|
-
};
|
|
1087
|
-
expect(isServiceCoveredByExisting(svc, fullCoverageCtx)).toBe(true);
|
|
1088
|
-
});
|
|
1089
|
-
|
|
1090
|
-
it('returns false when only some operations are covered', () => {
|
|
1091
|
-
const svc: Service = {
|
|
1092
|
-
name: 'Invoices',
|
|
1093
|
-
operations: [
|
|
1094
|
-
{
|
|
1095
|
-
name: 'listInvoices',
|
|
1096
|
-
httpMethod: 'get',
|
|
1097
|
-
path: '/invoices',
|
|
1098
|
-
pathParams: [],
|
|
1099
|
-
queryParams: [],
|
|
1100
|
-
headerParams: [],
|
|
1101
|
-
response: { kind: 'model', name: 'DirectoryList' },
|
|
1102
|
-
errors: [],
|
|
1103
|
-
injectIdempotencyKey: false,
|
|
1104
|
-
},
|
|
1105
|
-
{
|
|
1106
|
-
name: 'createInvoice',
|
|
1107
|
-
httpMethod: 'post',
|
|
1108
|
-
path: '/invoices',
|
|
1109
|
-
pathParams: [],
|
|
1110
|
-
queryParams: [],
|
|
1111
|
-
headerParams: [],
|
|
1112
|
-
response: { kind: 'model', name: 'Invoice' },
|
|
1113
|
-
errors: [],
|
|
1114
|
-
injectIdempotencyKey: false,
|
|
1115
|
-
},
|
|
1116
|
-
],
|
|
1117
|
-
};
|
|
1118
|
-
const partialCtx: EmitterContext = {
|
|
1119
|
-
namespace: 'workos',
|
|
1120
|
-
namespacePascal: 'WorkOS',
|
|
1121
|
-
spec: emptySpec,
|
|
1122
|
-
apiSurface: {
|
|
1123
|
-
language: 'node',
|
|
1124
|
-
extractedFrom: 'test',
|
|
1125
|
-
extractedAt: '2024-01-01',
|
|
1126
|
-
interfaces: {},
|
|
1127
|
-
classes: {
|
|
1128
|
-
Billing: {
|
|
1129
|
-
name: 'Billing',
|
|
1130
|
-
methods: {},
|
|
1131
|
-
properties: {},
|
|
1132
|
-
constructorParams: [],
|
|
1133
|
-
},
|
|
1134
|
-
},
|
|
1135
|
-
enums: {},
|
|
1136
|
-
typeAliases: {},
|
|
1137
|
-
exports: {},
|
|
1138
|
-
},
|
|
1139
|
-
overlayLookup: {
|
|
1140
|
-
methodByOperation: new Map([
|
|
1141
|
-
[
|
|
1142
|
-
'GET /invoices',
|
|
1143
|
-
{
|
|
1144
|
-
className: 'Billing',
|
|
1145
|
-
methodName: 'listInvoices',
|
|
1146
|
-
params: [],
|
|
1147
|
-
returnType: 'Promise<AutoPaginatable<Directory>>',
|
|
1148
|
-
},
|
|
1149
|
-
],
|
|
1150
|
-
]),
|
|
1151
|
-
httpKeyByMethod: new Map(),
|
|
1152
|
-
interfaceByName: new Map(),
|
|
1153
|
-
typeAliasByName: new Map(),
|
|
1154
|
-
requiredExports: new Map(),
|
|
1155
|
-
modelNameByIR: new Map(),
|
|
1156
|
-
fileBySymbol: new Map(),
|
|
1157
|
-
},
|
|
1158
|
-
};
|
|
1159
|
-
expect(isServiceCoveredByExisting(svc, partialCtx)).toBe(false);
|
|
164
|
+
expect(isServiceCoveredByExisting(service, ctxWithOverlay)).toBe(false);
|
|
1160
165
|
});
|
|
1161
166
|
|
|
1162
167
|
it('returns false for services with zero operations', () => {
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
operations: [],
|
|
1166
|
-
};
|
|
1167
|
-
const overlayCtx: EmitterContext = {
|
|
1168
|
-
namespace: 'workos',
|
|
1169
|
-
namespacePascal: 'WorkOS',
|
|
1170
|
-
spec: emptySpec,
|
|
1171
|
-
apiSurface: {
|
|
1172
|
-
language: 'node',
|
|
1173
|
-
extractedFrom: 'test',
|
|
1174
|
-
extractedAt: '2024-01-01',
|
|
1175
|
-
interfaces: {},
|
|
1176
|
-
classes: {
|
|
1177
|
-
Other: {
|
|
1178
|
-
name: 'Other',
|
|
1179
|
-
methods: {},
|
|
1180
|
-
properties: {},
|
|
1181
|
-
constructorParams: [],
|
|
1182
|
-
},
|
|
1183
|
-
},
|
|
1184
|
-
enums: {},
|
|
1185
|
-
typeAliases: {},
|
|
1186
|
-
exports: {},
|
|
1187
|
-
},
|
|
1188
|
-
overlayLookup: {
|
|
1189
|
-
methodByOperation: new Map([
|
|
1190
|
-
[
|
|
1191
|
-
'GET /something',
|
|
1192
|
-
{
|
|
1193
|
-
className: 'Other',
|
|
1194
|
-
methodName: 'doSomething',
|
|
1195
|
-
params: [],
|
|
1196
|
-
returnType: 'void',
|
|
1197
|
-
},
|
|
1198
|
-
],
|
|
1199
|
-
]),
|
|
1200
|
-
httpKeyByMethod: new Map(),
|
|
1201
|
-
interfaceByName: new Map(),
|
|
1202
|
-
typeAliasByName: new Map(),
|
|
1203
|
-
requiredExports: new Map(),
|
|
1204
|
-
modelNameByIR: new Map(),
|
|
1205
|
-
fileBySymbol: new Map(),
|
|
1206
|
-
},
|
|
1207
|
-
};
|
|
1208
|
-
expect(isServiceCoveredByExisting(emptySvc, overlayCtx)).toBe(false);
|
|
1209
|
-
});
|
|
1210
|
-
|
|
1211
|
-
it('returns false when overlay covers operations but target class is not in baseline', () => {
|
|
1212
|
-
const svc: Service = {
|
|
1213
|
-
name: 'Payments',
|
|
1214
|
-
operations: [
|
|
1215
|
-
{
|
|
1216
|
-
name: 'listPayments',
|
|
1217
|
-
httpMethod: 'get',
|
|
1218
|
-
path: '/payments',
|
|
1219
|
-
pathParams: [],
|
|
1220
|
-
queryParams: [],
|
|
1221
|
-
headerParams: [],
|
|
1222
|
-
response: { kind: 'model', name: 'ConnectionList' },
|
|
1223
|
-
errors: [],
|
|
1224
|
-
injectIdempotencyKey: false,
|
|
1225
|
-
},
|
|
1226
|
-
],
|
|
1227
|
-
};
|
|
1228
|
-
const missingClassCtx: EmitterContext = {
|
|
1229
|
-
namespace: 'workos',
|
|
1230
|
-
namespacePascal: 'WorkOS',
|
|
1231
|
-
spec: emptySpec,
|
|
1232
|
-
apiSurface: {
|
|
1233
|
-
language: 'node',
|
|
1234
|
-
extractedFrom: 'test',
|
|
1235
|
-
extractedAt: '2024-01-01',
|
|
1236
|
-
interfaces: {},
|
|
1237
|
-
classes: {},
|
|
1238
|
-
enums: {},
|
|
1239
|
-
typeAliases: {},
|
|
1240
|
-
exports: {},
|
|
1241
|
-
},
|
|
1242
|
-
overlayLookup: {
|
|
1243
|
-
methodByOperation: new Map([
|
|
1244
|
-
[
|
|
1245
|
-
'GET /payments',
|
|
1246
|
-
{
|
|
1247
|
-
className: 'Sso',
|
|
1248
|
-
methodName: 'listPayments',
|
|
1249
|
-
params: [],
|
|
1250
|
-
returnType: 'Promise<AutoPaginatable<Connection>>',
|
|
1251
|
-
},
|
|
1252
|
-
],
|
|
1253
|
-
]),
|
|
1254
|
-
httpKeyByMethod: new Map(),
|
|
1255
|
-
interfaceByName: new Map(),
|
|
1256
|
-
typeAliasByName: new Map(),
|
|
1257
|
-
requiredExports: new Map(),
|
|
1258
|
-
modelNameByIR: new Map(),
|
|
1259
|
-
fileBySymbol: new Map(),
|
|
1260
|
-
},
|
|
1261
|
-
};
|
|
1262
|
-
expect(isServiceCoveredByExisting(svc, missingClassCtx)).toBe(false);
|
|
168
|
+
const service: Service = { name: 'Empty', operations: [] };
|
|
169
|
+
expect(isServiceCoveredByExisting(service, ctx)).toBe(false);
|
|
1263
170
|
});
|
|
1264
171
|
|
|
1265
172
|
it('returns false when no apiSurface is provided', () => {
|
|
1266
|
-
const
|
|
1267
|
-
name: '
|
|
173
|
+
const service: Service = {
|
|
174
|
+
name: 'Organizations',
|
|
1268
175
|
operations: [
|
|
1269
176
|
{
|
|
1270
|
-
name: '
|
|
177
|
+
name: 'listOrganizations',
|
|
1271
178
|
httpMethod: 'get',
|
|
1272
|
-
path: '/
|
|
179
|
+
path: '/organizations',
|
|
1273
180
|
pathParams: [],
|
|
1274
181
|
queryParams: [],
|
|
1275
182
|
headerParams: [],
|
|
1276
|
-
response: { kind: '
|
|
183
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
1277
184
|
errors: [],
|
|
1278
185
|
injectIdempotencyKey: false,
|
|
1279
186
|
},
|
|
1280
187
|
],
|
|
1281
188
|
};
|
|
1282
|
-
const
|
|
1283
|
-
|
|
1284
|
-
namespacePascal: 'WorkOS',
|
|
1285
|
-
spec: emptySpec,
|
|
189
|
+
const ctxWithOverlayNoSurface: EmitterContext = {
|
|
190
|
+
...ctx,
|
|
1286
191
|
overlayLookup: {
|
|
1287
192
|
methodByOperation: new Map([
|
|
1288
193
|
[
|
|
1289
|
-
'GET /
|
|
194
|
+
'GET /organizations',
|
|
1290
195
|
{
|
|
1291
|
-
className: '
|
|
1292
|
-
methodName: '
|
|
196
|
+
className: 'Organizations',
|
|
197
|
+
methodName: 'listOrganizations',
|
|
1293
198
|
params: [],
|
|
1294
|
-
returnType: 'Promise<AutoPaginatable<
|
|
199
|
+
returnType: 'Promise<AutoPaginatable<Organization>>',
|
|
1295
200
|
},
|
|
1296
201
|
],
|
|
1297
202
|
]),
|
|
@@ -1303,6 +208,6 @@ describe('isServiceCoveredByExisting', () => {
|
|
|
1303
208
|
fileBySymbol: new Map(),
|
|
1304
209
|
},
|
|
1305
210
|
};
|
|
1306
|
-
expect(isServiceCoveredByExisting(
|
|
211
|
+
expect(isServiceCoveredByExisting(service, ctxWithOverlayNoSurface)).toBe(false);
|
|
1307
212
|
});
|
|
1308
213
|
});
|