@workos/oagen-emitters 0.12.2 → 0.12.3
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/.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-eCuvoL1T.mjs → plugin-D2N2ZT5W.mjs} +63 -24
- package/dist/plugin-D2N2ZT5W.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +4 -4
- package/renovate.json +1 -1
- package/src/node/index.ts +11 -2
- package/src/node/models.ts +7 -0
- package/src/node/naming.ts +3 -0
- package/src/node/path-expression.ts +11 -4
- package/src/node/resources.ts +23 -7
- package/src/node/tests.ts +44 -10
- package/test/node/resources.test.ts +70 -0
- package/test/node/tests.test.ts +95 -0
- package/dist/plugin-eCuvoL1T.mjs.map +0 -1
package/dist/plugin.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as workosEmittersPlugin } from "./plugin-
|
|
1
|
+
import { t as workosEmittersPlugin } from "./plugin-D2N2ZT5W.mjs";
|
|
2
2
|
export { workosEmittersPlugin };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workos/oagen-emitters",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.3",
|
|
4
4
|
"description": "WorkOS' oagen emitters",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "WorkOS",
|
|
@@ -40,13 +40,13 @@
|
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@commitlint/cli": "^21.0.1",
|
|
42
42
|
"@commitlint/config-conventional": "^21.0.1",
|
|
43
|
-
"@types/node": "^25.
|
|
43
|
+
"@types/node": "^25.9.0",
|
|
44
44
|
"husky": "^9.1.7",
|
|
45
45
|
"oxfmt": "^0.50.0",
|
|
46
46
|
"oxlint": "^1.65.0",
|
|
47
47
|
"prettier": "^3.8.3",
|
|
48
48
|
"tsdown": "^0.22.0",
|
|
49
|
-
"tsx": "^4.22.
|
|
49
|
+
"tsx": "^4.22.2",
|
|
50
50
|
"typescript": "^6.0.3",
|
|
51
51
|
"vitest": "^4.1.6"
|
|
52
52
|
},
|
|
@@ -54,6 +54,6 @@
|
|
|
54
54
|
"node": ">=24.10.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@workos/oagen": "^0.19.
|
|
57
|
+
"@workos/oagen": "^0.19.1"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/renovate.json
CHANGED
package/src/node/index.ts
CHANGED
|
@@ -315,10 +315,19 @@ function applyLiveSurface(files: GeneratedFile[], ctx: EmitterContext, surface:
|
|
|
315
315
|
// user has hand-tuned, or leave it pointing at sibling files that no
|
|
316
316
|
// longer have the same shape. Treat existing test/fixture files as
|
|
317
317
|
// frozen even when they carry the auto-gen header.
|
|
318
|
+
//
|
|
319
|
+
// Adopted-service directories are treated like owned dirs for this
|
|
320
|
+
// purpose: adoption means oagen created the directory from scratch, so
|
|
321
|
+
// by construction there is no hand-written content to preserve and
|
|
322
|
+
// emitting tests/fixtures is safe. Rule (a) still drops files that
|
|
323
|
+
// somehow exist hand-written.
|
|
318
324
|
if (isUserOwnedAfterFirstEmit(f.path)) {
|
|
325
|
+
const dir = topLevelDir(f.path);
|
|
326
|
+
const isAdoptedDir = dir !== undefined && policy.adoptedServiceDirs.has(dir);
|
|
327
|
+
const isManagedDir = ownedPath || isAdoptedDir;
|
|
319
328
|
if (surface.files.has(f.path) && !surface.autogenFiles.has(f.path)) continue;
|
|
320
|
-
if (!
|
|
321
|
-
if (
|
|
329
|
+
if (!isManagedDir && !surface.autogenFiles.has(f.path)) continue;
|
|
330
|
+
if (isManagedDir && !policy.regenerateOwnedTests) continue;
|
|
322
331
|
}
|
|
323
332
|
|
|
324
333
|
// Previously auto-generated files → fully overwrite so spec changes
|
package/src/node/models.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
resolveInterfaceName,
|
|
11
11
|
wireInterfaceName,
|
|
12
12
|
resolveMethodName,
|
|
13
|
+
isAdoptedModelName,
|
|
13
14
|
} from './naming.js';
|
|
14
15
|
import {
|
|
15
16
|
collectFieldDependencies,
|
|
@@ -119,6 +120,12 @@ function isSupportedFieldType(
|
|
|
119
120
|
if (ref.name === ownerModelName) return true;
|
|
120
121
|
const resolvedName = resolveInterfaceName(ref.name, ctx);
|
|
121
122
|
if (ctx.apiSurface?.interfaces?.[resolvedName] || ctx.apiSurface?.typeAliases?.[resolvedName]) return true;
|
|
123
|
+
// Adopted-service models will have their interfaces emitted in this
|
|
124
|
+
// same pass, so the field reference will resolve once writing is done.
|
|
125
|
+
// Without this, fields like `UserManagementLoginRequest.user` get
|
|
126
|
+
// silently dropped on first emission because the target interface
|
|
127
|
+
// (`UserObject` under the adopted `connect/` dir) hasn't landed yet.
|
|
128
|
+
if (isAdoptedModelName(ref.name)) return true;
|
|
122
129
|
const relPath = `src/${shared.resolveDir(shared.modelToService.get(ref.name))}/interfaces/${fileName(ref.name)}.interface.ts`;
|
|
123
130
|
return liveSurfaceHasManagedFile(relPath);
|
|
124
131
|
}
|
package/src/node/naming.ts
CHANGED
|
@@ -65,6 +65,9 @@ let adoptedModelNames: Set<string> = new Set();
|
|
|
65
65
|
export function setAdoptedModelNames(names: Set<string>): void {
|
|
66
66
|
adoptedModelNames = names;
|
|
67
67
|
}
|
|
68
|
+
export function isAdoptedModelName(name: string): boolean {
|
|
69
|
+
return adoptedModelNames.has(name);
|
|
70
|
+
}
|
|
68
71
|
|
|
69
72
|
/**
|
|
70
73
|
* Wire/response interface name.
|
|
@@ -14,8 +14,14 @@ import { fieldName } from './naming.js';
|
|
|
14
14
|
* "/orgs" → `'orgs'`
|
|
15
15
|
* "/orgs/{id}" → `` `orgs/${encodeURIComponent(id)}` ``
|
|
16
16
|
* "/orgs/{id}/foo" → `` `orgs/${encodeURIComponent(id)}/foo` ``
|
|
17
|
+
*
|
|
18
|
+
* `paramNameMap` lets a caller override the local variable name used for a
|
|
19
|
+
* spec parameter — used by the options-object code path so the URL template
|
|
20
|
+
* references the SDK's public field name (e.g. `organizationMembershipId`)
|
|
21
|
+
* instead of the spec's path-param name (e.g. `omId`), avoiding a
|
|
22
|
+
* destructure rename in the method body.
|
|
17
23
|
*/
|
|
18
|
-
export function buildNodePathExpression(rawPath: string): string {
|
|
24
|
+
export function buildNodePathExpression(rawPath: string, paramNameMap?: Map<string, string>): string {
|
|
19
25
|
const segments = parsePathTemplate(rawPath);
|
|
20
26
|
if (!hasPathParams(segments)) {
|
|
21
27
|
return `'${rawPath}'`;
|
|
@@ -23,15 +29,16 @@ export function buildNodePathExpression(rawPath: string): string {
|
|
|
23
29
|
|
|
24
30
|
let body = '';
|
|
25
31
|
for (const seg of segments) {
|
|
26
|
-
body += renderSegment(seg);
|
|
32
|
+
body += renderSegment(seg, paramNameMap);
|
|
27
33
|
}
|
|
28
34
|
return `\`${body}\``;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
function renderSegment(seg: PathSegment): string {
|
|
37
|
+
function renderSegment(seg: PathSegment, paramNameMap?: Map<string, string>): string {
|
|
32
38
|
if (seg.kind === 'literal') {
|
|
33
39
|
// Template-literal-safe escapes: backtick, backslash, ${
|
|
34
40
|
return seg.value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
35
41
|
}
|
|
36
|
-
|
|
42
|
+
const localName = paramNameMap?.get(seg.name) ?? fieldName(seg.name);
|
|
43
|
+
return `\${encodeURIComponent(${localName})}`;
|
|
37
44
|
}
|
package/src/node/resources.ts
CHANGED
|
@@ -1242,8 +1242,8 @@ function renderOptionsObjectMethod(
|
|
|
1242
1242
|
if (!optionParam) return false;
|
|
1243
1243
|
|
|
1244
1244
|
const responseModel = plan.responseModelName ? resolveInterfaceName(plan.responseModelName, ctx) : null;
|
|
1245
|
-
const pathStr = buildPathStr(op);
|
|
1246
1245
|
const pathBindings = buildOptionsObjectPathBindings(op, optionParam.type, ctx);
|
|
1246
|
+
const pathStr = buildPathStr(op, buildOptionsObjectPathParamMap(op, optionParam.type, ctx));
|
|
1247
1247
|
|
|
1248
1248
|
if (plan.isPaginated && op.pagination && op.httpMethod === 'get') {
|
|
1249
1249
|
let itemRawName = op.pagination.itemType.kind === 'model' ? op.pagination.itemType.name : null;
|
|
@@ -1374,11 +1374,27 @@ function renderOptionsObjectDestructure(lines: string[], pathBindings: string[],
|
|
|
1374
1374
|
}
|
|
1375
1375
|
|
|
1376
1376
|
function buildOptionsObjectPathBindings(op: Operation, optionType: string, ctx: EmitterContext): string[] {
|
|
1377
|
-
|
|
1377
|
+
// Return resolved SDK field names directly — the URL template uses these
|
|
1378
|
+
// names too (via the param-name map threaded into buildNodePathExpression),
|
|
1379
|
+
// so the destructure no longer needs `optionField: localName` renames.
|
|
1380
|
+
return op.pathParams.map((param) => resolveOptionsObjectField(fieldName(param.name), optionType, ctx));
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Map spec path-param names (e.g. `omId`) to the SDK field name exposed on
|
|
1385
|
+
* the options interface (e.g. `organizationMembershipId`). Empty when every
|
|
1386
|
+
* path param's spec name already matches the SDK field. Consumed by
|
|
1387
|
+
* `buildNodePathExpression` so the URL template binds to the same identifier
|
|
1388
|
+
* the destructure does.
|
|
1389
|
+
*/
|
|
1390
|
+
function buildOptionsObjectPathParamMap(op: Operation, optionType: string, ctx: EmitterContext): Map<string, string> {
|
|
1391
|
+
const map = new Map<string, string>();
|
|
1392
|
+
for (const param of op.pathParams) {
|
|
1378
1393
|
const localName = fieldName(param.name);
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1381
|
-
}
|
|
1394
|
+
const sdkField = resolveOptionsObjectField(localName, optionType, ctx);
|
|
1395
|
+
if (sdkField !== localName) map.set(param.name, sdkField);
|
|
1396
|
+
}
|
|
1397
|
+
return map;
|
|
1382
1398
|
}
|
|
1383
1399
|
|
|
1384
1400
|
function resolveOptionsObjectField(localName: string, optionType: string, ctx: EmitterContext): string {
|
|
@@ -1826,8 +1842,8 @@ function renderQueryExpr(queryParams: { name: string; required: boolean }[]): st
|
|
|
1826
1842
|
return `options ? { ${parts.join(', ')} } : undefined`;
|
|
1827
1843
|
}
|
|
1828
1844
|
|
|
1829
|
-
function buildPathStr(op: Operation): string {
|
|
1830
|
-
return buildNodePathExpression(op.path);
|
|
1845
|
+
function buildPathStr(op: Operation, paramNameMap?: Map<string, string>): string {
|
|
1846
|
+
return buildNodePathExpression(op.path, paramNameMap);
|
|
1831
1847
|
}
|
|
1832
1848
|
|
|
1833
1849
|
function buildPathParams(op: Operation, specEnumNames?: Set<string>): string {
|
package/src/node/tests.ts
CHANGED
|
@@ -110,6 +110,15 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
110
110
|
const propName = mountAccessors.get(mountName) ?? servicePropertyName(mountName);
|
|
111
111
|
if (ctx.apiSurface && baselineWorkOSProps.size > 0 && !baselineWorkOSProps.has(propName)) continue;
|
|
112
112
|
|
|
113
|
+
// Skip when the resource class diverges from the mount accessor — this
|
|
114
|
+
// happens for services with constructor-incompatible baselines (e.g.
|
|
115
|
+
// hand-written `Webhooks(crypto)` forks the emitted ops onto
|
|
116
|
+
// `WebhooksEndpoints`). The generated test would do
|
|
117
|
+
// `workos.<propName>.fooMethod(...)`, but those methods live on a
|
|
118
|
+
// different class, so the test would fail to compile.
|
|
119
|
+
const resourceClass = resolveResourceClassName(mergedService, ctx);
|
|
120
|
+
if (resourceClass !== mountName) continue;
|
|
121
|
+
|
|
113
122
|
const testService = ops.length < operations.length ? { ...mergedService, operations: ops } : mergedService;
|
|
114
123
|
files.push(generateServiceTest(testService, spec, ctx, modelMap, mountAccessors));
|
|
115
124
|
}
|
|
@@ -409,8 +418,12 @@ function renderBodyTest(
|
|
|
409
418
|
const optionsArg = buildOptionsObjectTestArg(op, plan, baselineMethod, modelMap, ctx);
|
|
410
419
|
const allArgs = optionsArg ?? (pathArgs ? `${pathArgs}, ${payloadArg}` : payloadArg);
|
|
411
420
|
|
|
421
|
+
const isArrayResponse = !!plan.isArrayResponse;
|
|
422
|
+
const fixtureExpr = isArrayResponse ? `[${fixture}]` : fixture;
|
|
423
|
+
const accessor = isArrayResponse ? 'result[0]' : 'result';
|
|
424
|
+
|
|
412
425
|
lines.push(" it('sends the correct request and returns result', async () => {");
|
|
413
|
-
lines.push(` fetchOnce(${
|
|
426
|
+
lines.push(` fetchOnce(${fixtureExpr});`);
|
|
414
427
|
lines.push('');
|
|
415
428
|
lines.push(` const result = await workos.${serviceProp}.${method}(${allArgs});`);
|
|
416
429
|
lines.push('');
|
|
@@ -427,14 +440,18 @@ function renderBodyTest(
|
|
|
427
440
|
lines.push(' expect(fetchBody()).toBeDefined();');
|
|
428
441
|
}
|
|
429
442
|
|
|
443
|
+
if (isArrayResponse) {
|
|
444
|
+
lines.push(' expect(Array.isArray(result)).toBe(true);');
|
|
445
|
+
}
|
|
446
|
+
|
|
430
447
|
// Use entity helper if available, otherwise inline assertions
|
|
431
448
|
const bodyHelperName = ctx ? `expect${resolveInterfaceName(responseModelName, ctx)}` : null;
|
|
432
449
|
if (bodyHelperName && entityHelpers?.has(bodyHelperName)) {
|
|
433
|
-
lines.push(` ${bodyHelperName}(
|
|
450
|
+
lines.push(` ${bodyHelperName}(${accessor});`);
|
|
434
451
|
} else {
|
|
435
452
|
const responseModel = modelMap.get(responseModelName);
|
|
436
453
|
if (responseModel) {
|
|
437
|
-
const assertions = buildFieldAssertions(responseModel,
|
|
454
|
+
const assertions = buildFieldAssertions(responseModel, accessor, modelMap);
|
|
438
455
|
if (assertions.length > 0) {
|
|
439
456
|
for (const assertion of assertions) {
|
|
440
457
|
lines.push(` ${assertion}`);
|
|
@@ -466,8 +483,12 @@ function renderGetTest(
|
|
|
466
483
|
const pathArgs = buildTestPathArgs(op);
|
|
467
484
|
const optionsArg = buildOptionsObjectTestArg(op, plan, baselineMethod, modelMap, ctx);
|
|
468
485
|
|
|
486
|
+
const isArrayResponse = !!plan.isArrayResponse;
|
|
487
|
+
const fixtureExpr = isArrayResponse ? `[${fixture}]` : fixture;
|
|
488
|
+
const accessor = isArrayResponse ? 'result[0]' : 'result';
|
|
489
|
+
|
|
469
490
|
lines.push(" it('returns the expected result', async () => {");
|
|
470
|
-
lines.push(` fetchOnce(${
|
|
491
|
+
lines.push(` fetchOnce(${fixtureExpr});`);
|
|
471
492
|
lines.push('');
|
|
472
493
|
lines.push(` const result = await workos.${serviceProp}.${method}(${optionsArg ?? pathArgs});`);
|
|
473
494
|
lines.push('');
|
|
@@ -475,15 +496,18 @@ function renderGetTest(
|
|
|
475
496
|
// Fix #12: Full URL path assertion instead of toContain()
|
|
476
497
|
const expectedPathGet = buildExpectedPath(op);
|
|
477
498
|
lines.push(` expect(new URL(String(fetchURL())).pathname).toBe('${expectedPathGet}');`);
|
|
499
|
+
if (isArrayResponse) {
|
|
500
|
+
lines.push(' expect(Array.isArray(result)).toBe(true);');
|
|
501
|
+
}
|
|
478
502
|
|
|
479
503
|
// Use entity helper if available, otherwise inline assertions
|
|
480
504
|
const helperName = ctx ? `expect${resolveInterfaceName(responseModelName, ctx)}` : null;
|
|
481
505
|
if (helperName && entityHelpers?.has(helperName)) {
|
|
482
|
-
lines.push(` ${helperName}(
|
|
506
|
+
lines.push(` ${helperName}(${accessor});`);
|
|
483
507
|
} else {
|
|
484
508
|
const responseModel = modelMap.get(responseModelName);
|
|
485
509
|
if (responseModel) {
|
|
486
|
-
const assertions = buildFieldAssertions(responseModel,
|
|
510
|
+
const assertions = buildFieldAssertions(responseModel, accessor, modelMap);
|
|
487
511
|
if (assertions.length > 0) {
|
|
488
512
|
for (const assertion of assertions) {
|
|
489
513
|
lines.push(` ${assertion}`);
|
|
@@ -636,27 +660,37 @@ function generateEntityHelpers(
|
|
|
636
660
|
* nested models so we still get meaningful assertions instead of a bare
|
|
637
661
|
* `toBeDefined()`.
|
|
638
662
|
*/
|
|
663
|
+
function isDateTimeFieldType(type: TypeRef): boolean {
|
|
664
|
+
if (type.kind === 'primitive') return type.format === 'date-time';
|
|
665
|
+
if (type.kind === 'nullable') return isDateTimeFieldType(type.inner);
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
|
|
639
669
|
function buildFieldAssertions(model: Model, accessor: string, modelMap?: Map<string, Model>): string[] {
|
|
640
670
|
const assertions: string[] = [];
|
|
641
671
|
|
|
642
672
|
for (const field of model.fields) {
|
|
643
673
|
if (!field.required) continue;
|
|
674
|
+
const domainField = fieldName(field.name);
|
|
675
|
+
// `string` + `format: 'date-time'` is deserialized to `Date` by the
|
|
676
|
+
// serializer (see `mapPrimitive` in type-map.ts). Asserting against a
|
|
677
|
+
// string literal would fail Object.is — compare via `.toISOString()`.
|
|
678
|
+
const isDateTime = isDateTimeFieldType(field.type);
|
|
679
|
+
const fieldAccessor = isDateTime ? `${accessor}.${domainField}.toISOString()` : `${accessor}.${domainField}`;
|
|
644
680
|
// When a field has an example value, use it as the expected assertion value
|
|
645
681
|
if (field.example !== undefined) {
|
|
646
|
-
const domainField = fieldName(field.name);
|
|
647
682
|
if (typeof field.example === 'object' && field.example !== null) {
|
|
648
683
|
// Objects and arrays need toEqual with JSON serialization
|
|
649
684
|
assertions.push(`expect(${accessor}.${domainField}).toEqual(${JSON.stringify(field.example)});`);
|
|
650
685
|
} else {
|
|
651
686
|
const exampleLiteral = typeof field.example === 'string' ? `'${field.example}'` : String(field.example);
|
|
652
|
-
assertions.push(`expect(${
|
|
687
|
+
assertions.push(`expect(${fieldAccessor}).toBe(${exampleLiteral});`);
|
|
653
688
|
}
|
|
654
689
|
continue;
|
|
655
690
|
}
|
|
656
691
|
const value = fixtureValueForType(field.type, field.name, model.name);
|
|
657
692
|
if (value === null) continue;
|
|
658
|
-
|
|
659
|
-
assertions.push(`expect(${accessor}.${domainField}).toBe(${value});`);
|
|
693
|
+
assertions.push(`expect(${fieldAccessor}).toBe(${value});`);
|
|
660
694
|
}
|
|
661
695
|
|
|
662
696
|
// When no primitive assertions were found (e.g. wrapper types like
|
|
@@ -176,6 +176,76 @@ describe('generateResources', () => {
|
|
|
176
176
|
expect(resourceFile!.content).toContain('async listGroupsForOrganizationMembership');
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
+
it('options-object: URL template binds to the SDK field name, not the spec path-param name', () => {
|
|
180
|
+
// When the spec uses `omId` as a path-param name but the baseline options
|
|
181
|
+
// interface exposes `organizationMembershipId`, both the destructure and
|
|
182
|
+
// the URL template should reference `organizationMembershipId` directly —
|
|
183
|
+
// no `organizationMembershipId: omId` rename indirection in the body.
|
|
184
|
+
const operation = {
|
|
185
|
+
name: 'removeOrganizationMembership',
|
|
186
|
+
httpMethod: 'delete' as const,
|
|
187
|
+
path: '/organizations/{organizationId}/groups/{groupId}/organization-memberships/{omId}',
|
|
188
|
+
pathParams: [
|
|
189
|
+
{ name: 'organizationId', type: { kind: 'primitive' as const, type: 'string' as const }, required: true },
|
|
190
|
+
{ name: 'groupId', type: { kind: 'primitive' as const, type: 'string' as const }, required: true },
|
|
191
|
+
{ name: 'omId', type: { kind: 'primitive' as const, type: 'string' as const }, required: true },
|
|
192
|
+
],
|
|
193
|
+
queryParams: [],
|
|
194
|
+
headerParams: [],
|
|
195
|
+
response: { kind: 'primitive' as const, type: 'unknown' as const },
|
|
196
|
+
errors: [],
|
|
197
|
+
injectIdempotencyKey: false,
|
|
198
|
+
};
|
|
199
|
+
const service: Service = { name: 'Groups', operations: [operation] };
|
|
200
|
+
const spec: ApiSpec = { ...emptySpec, services: [service] };
|
|
201
|
+
const ctxWithBaseline: EmitterContext = {
|
|
202
|
+
...ctx,
|
|
203
|
+
spec,
|
|
204
|
+
emitterOptions: { ownedServices: ['Groups'] },
|
|
205
|
+
apiSurface: {
|
|
206
|
+
classes: {
|
|
207
|
+
Groups: {
|
|
208
|
+
constructorParams: [{ name: 'workos', type: 'WorkOS' }],
|
|
209
|
+
methods: {
|
|
210
|
+
removeOrganizationMembership: [
|
|
211
|
+
{
|
|
212
|
+
name: 'removeOrganizationMembership',
|
|
213
|
+
params: [
|
|
214
|
+
{
|
|
215
|
+
name: 'options',
|
|
216
|
+
type: 'RemoveGroupOrganizationMembershipOptions',
|
|
217
|
+
passingStyle: 'options_object',
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
returnType: 'Promise<void>',
|
|
221
|
+
async: true,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
interfaces: {
|
|
228
|
+
RemoveGroupOrganizationMembershipOptions: {
|
|
229
|
+
fields: {
|
|
230
|
+
organizationId: { type: 'string', required: true },
|
|
231
|
+
groupId: { type: 'string', required: true },
|
|
232
|
+
organizationMembershipId: { type: 'string', required: true },
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
} as any,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const result = generateResources([service], ctxWithBaseline);
|
|
240
|
+
const resourceFile = result.find((f) => f.path === 'src/groups/groups.ts');
|
|
241
|
+
expect(resourceFile).toBeDefined();
|
|
242
|
+
const content = resourceFile!.content;
|
|
243
|
+
expect(content).toContain('const { organizationId, groupId, organizationMembershipId } = options;');
|
|
244
|
+
expect(content).toContain('${encodeURIComponent(organizationMembershipId)}');
|
|
245
|
+
expect(content).not.toContain('organizationMembershipId: omId');
|
|
246
|
+
expect(content).not.toContain('encodeURIComponent(omId)');
|
|
247
|
+
});
|
|
248
|
+
|
|
179
249
|
it('drops brand-new service paths in an existing SDK by default', () => {
|
|
180
250
|
const tmpRoot = createTrackedSdkRoot();
|
|
181
251
|
try {
|
package/test/node/tests.test.ts
CHANGED
|
@@ -116,4 +116,99 @@ describe('node test generation ownership', () => {
|
|
|
116
116
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
it('emits tests and fixtures for adopted services when regenerateOwnedTests is true', () => {
|
|
121
|
+
// Adopted dirs are created by oagen from scratch — no hand-written content
|
|
122
|
+
// to preserve, so scaffolding tests/fixtures is safe and useful.
|
|
123
|
+
const tmpRoot = createTrackedSdkRoot();
|
|
124
|
+
try {
|
|
125
|
+
const result = nodeEmitter.generateTests!(spec, {
|
|
126
|
+
...ctx,
|
|
127
|
+
outputDir: tmpRoot,
|
|
128
|
+
emitterOptions: { adoptMissingServices: true, regenerateOwnedTests: true },
|
|
129
|
+
} as EmitterContext);
|
|
130
|
+
|
|
131
|
+
const testFile = result.find((f) => f.path === 'src/groups/groups.spec.ts');
|
|
132
|
+
const fixtureFile = result.find((f) => f.path === 'src/groups/fixtures/group.json');
|
|
133
|
+
expect(testFile).toBeDefined();
|
|
134
|
+
expect(fixtureFile).toBeDefined();
|
|
135
|
+
} finally {
|
|
136
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('skips tests for adopted services when regenerateOwnedTests is false', () => {
|
|
141
|
+
const tmpRoot = createTrackedSdkRoot();
|
|
142
|
+
try {
|
|
143
|
+
const result = nodeEmitter.generateTests!(spec, {
|
|
144
|
+
...ctx,
|
|
145
|
+
outputDir: tmpRoot,
|
|
146
|
+
emitterOptions: { adoptMissingServices: true },
|
|
147
|
+
} as EmitterContext);
|
|
148
|
+
|
|
149
|
+
expect(result.some((f) => f.path.startsWith('src/groups/'))).toBe(false);
|
|
150
|
+
} finally {
|
|
151
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('skips tests when the resource class diverges from the mount accessor', () => {
|
|
156
|
+
// Hand-written `Webhooks` has a constructor incompatible with WorkOS,
|
|
157
|
+
// so the emitter forks endpoint methods onto a `WebhooksEndpoints`
|
|
158
|
+
// helper class. A generated test would call `workos.webhooks.foo(...)`
|
|
159
|
+
// — but those methods live on the helper, not the crypto class. Skip
|
|
160
|
+
// these tests so we don't ship a spec that fails to compile.
|
|
161
|
+
const webhookOp = {
|
|
162
|
+
name: 'listWebhookEndpoints',
|
|
163
|
+
httpMethod: 'get' as const,
|
|
164
|
+
path: '/webhook_endpoints',
|
|
165
|
+
pathParams: [],
|
|
166
|
+
queryParams: [],
|
|
167
|
+
headerParams: [],
|
|
168
|
+
response: { kind: 'primitive' as const, type: 'unknown' as const },
|
|
169
|
+
errors: [],
|
|
170
|
+
injectIdempotencyKey: false,
|
|
171
|
+
};
|
|
172
|
+
const webhookService: Service = { name: 'Webhooks', operations: [webhookOp] };
|
|
173
|
+
const webhookSpec: ApiSpec = {
|
|
174
|
+
...spec,
|
|
175
|
+
services: [webhookService],
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const tmpRoot = createTrackedSdkRoot();
|
|
179
|
+
try {
|
|
180
|
+
const result = nodeEmitter.generateTests!(webhookSpec, {
|
|
181
|
+
...ctx,
|
|
182
|
+
spec: webhookSpec,
|
|
183
|
+
outputDir: tmpRoot,
|
|
184
|
+
emitterOptions: { adoptMissingServices: true, regenerateOwnedTests: true },
|
|
185
|
+
apiSurface: {
|
|
186
|
+
classes: {
|
|
187
|
+
Webhooks: {
|
|
188
|
+
constructorParams: [{ name: 'crypto', type: 'CryptoProvider' }],
|
|
189
|
+
},
|
|
190
|
+
WorkOS: {
|
|
191
|
+
properties: { webhooks: { type: 'Webhooks' } },
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
resolvedOperations: [
|
|
196
|
+
{
|
|
197
|
+
operation: webhookOp,
|
|
198
|
+
service: webhookService,
|
|
199
|
+
methodName: 'list_webhook_endpoints',
|
|
200
|
+
mountOn: 'Webhooks',
|
|
201
|
+
defaults: {},
|
|
202
|
+
inferFromClient: [],
|
|
203
|
+
urlBuilder: false,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
} as unknown as EmitterContext);
|
|
207
|
+
|
|
208
|
+
expect(result.some((f) => f.path === 'src/webhooks/webhooks-endpoints.spec.ts')).toBe(false);
|
|
209
|
+
expect(result.some((f) => f.path === 'src/webhooks/webhooks.spec.ts')).toBe(false);
|
|
210
|
+
} finally {
|
|
211
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
119
214
|
});
|