@workos/oagen-emitters 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.oxfmtrc.json +8 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +633 -85
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/smoke/sdk-dotnet.ts +17 -3
- package/smoke/sdk-elixir.ts +17 -3
- package/smoke/sdk-go.ts +21 -4
- package/smoke/sdk-kotlin.ts +23 -4
- package/smoke/sdk-node.ts +15 -3
- package/smoke/sdk-ruby.ts +17 -3
- package/smoke/sdk-rust.ts +16 -3
- package/src/node/client.ts +94 -12
- package/src/node/common.ts +1 -1
- package/src/node/enums.ts +4 -4
- package/src/node/errors.ts +5 -1
- package/src/node/fixtures.ts +6 -4
- package/src/node/index.ts +65 -9
- package/src/node/models.ts +86 -75
- package/src/node/naming.ts +91 -2
- package/src/node/resources.ts +462 -23
- package/src/node/serializers.ts +3 -1
- package/src/node/tests.ts +39 -15
- package/src/node/utils.ts +52 -2
- package/test/node/client.test.ts +181 -82
- package/test/node/enums.test.ts +73 -3
- package/test/node/models.test.ts +107 -20
- package/test/node/naming.test.ts +14 -4
- package/test/node/resources.test.ts +627 -25
- package/test/node/serializers.test.ts +33 -6
package/src/node/tests.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
fieldName,
|
|
6
6
|
wireFieldName,
|
|
7
7
|
fileName,
|
|
8
|
-
|
|
8
|
+
resolveServiceDir,
|
|
9
9
|
servicePropertyName,
|
|
10
10
|
resolveMethodName,
|
|
11
11
|
resolveInterfaceName,
|
|
@@ -28,7 +28,7 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
28
28
|
// Generate fixture JSON files
|
|
29
29
|
const fixtures = generateFixtures(spec, ctx);
|
|
30
30
|
for (const f of fixtures) {
|
|
31
|
-
files.push({ path: f.path, content: f.content, headerPlacement: 'skip' });
|
|
31
|
+
files.push({ path: f.path, content: f.content, headerPlacement: 'skip', integrateTarget: false });
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// Build model lookup for response field assertions
|
|
@@ -61,7 +61,7 @@ function generateServiceTest(
|
|
|
61
61
|
modelMap: Map<string, Model>,
|
|
62
62
|
): GeneratedFile {
|
|
63
63
|
const resolvedName = resolveResourceClassName(service, ctx);
|
|
64
|
-
const serviceDir =
|
|
64
|
+
const serviceDir = resolveServiceDir(resolvedName);
|
|
65
65
|
const serviceClass = resolvedName;
|
|
66
66
|
const serviceProp = servicePropertyName(resolvedName);
|
|
67
67
|
const testPath = `src/${serviceDir}/${fileName(resolvedName)}.spec.ts`;
|
|
@@ -510,8 +510,9 @@ function buildCallArgs(op: Operation, plan: any, modelMap: Map<string, Model>):
|
|
|
510
510
|
|
|
511
511
|
if (isPaginated) return pathArgs || '';
|
|
512
512
|
if (hasBody) {
|
|
513
|
-
const
|
|
514
|
-
|
|
513
|
+
const payload = buildTestPayload(op, modelMap);
|
|
514
|
+
const bodyArg = payload ? payload.camelCaseObj : fallbackBodyArg(op, modelMap);
|
|
515
|
+
return pathArgs ? `${pathArgs}, ${bodyArg}` : bodyArg;
|
|
515
516
|
}
|
|
516
517
|
return pathArgs || '';
|
|
517
518
|
}
|
|
@@ -626,10 +627,18 @@ function buildFieldAssertions(model: Model, accessor: string, modelMap?: Map<str
|
|
|
626
627
|
}
|
|
627
628
|
|
|
628
629
|
/**
|
|
629
|
-
* Return a JS literal string for the expected fixture value of a
|
|
630
|
-
* Returns null for
|
|
630
|
+
* Return a JS literal string for the expected fixture value of a field.
|
|
631
|
+
* Returns null for types that cannot be deterministically generated.
|
|
632
|
+
* When a modelMap is provided, recursively builds object literals for nested model types.
|
|
633
|
+
* When wire is true, uses snake_case keys for nested model objects (wire format).
|
|
631
634
|
*/
|
|
632
|
-
function fixtureValueForType(
|
|
635
|
+
function fixtureValueForType(
|
|
636
|
+
ref: TypeRef,
|
|
637
|
+
name: string,
|
|
638
|
+
modelName: string,
|
|
639
|
+
modelMap?: Map<string, Model>,
|
|
640
|
+
wire?: boolean,
|
|
641
|
+
): string | null {
|
|
633
642
|
switch (ref.kind) {
|
|
634
643
|
case 'primitive':
|
|
635
644
|
return fixtureValueForPrimitive(ref.type, ref.format, name, modelName);
|
|
@@ -646,10 +655,24 @@ function fixtureValueForType(ref: TypeRef, name: string, modelName: string): str
|
|
|
646
655
|
// For arrays of primitives/enums, generate a single-element array assertion.
|
|
647
656
|
// For arrays of models/complex types, return null to skip the assertion —
|
|
648
657
|
// the fixture will have populated items that we can't predict here.
|
|
649
|
-
const itemValue = fixtureValueForType(ref.items, name, modelName);
|
|
658
|
+
const itemValue = fixtureValueForType(ref.items, name, modelName, modelMap, wire);
|
|
650
659
|
if (itemValue !== null) return `[${itemValue}]`;
|
|
651
660
|
return null;
|
|
652
661
|
}
|
|
662
|
+
case 'model': {
|
|
663
|
+
if (!modelMap) return null;
|
|
664
|
+
const nested = modelMap.get(ref.name);
|
|
665
|
+
if (!nested) return null;
|
|
666
|
+
const requiredFields = nested.fields.filter((f) => f.required);
|
|
667
|
+
const entries: string[] = [];
|
|
668
|
+
for (const field of requiredFields) {
|
|
669
|
+
const value = fixtureValueForType(field.type, field.name, nested.name, modelMap, wire);
|
|
670
|
+
if (value === null) return null; // Can't build a complete object
|
|
671
|
+
const key = wire ? wireFieldName(field.name) : fieldName(field.name);
|
|
672
|
+
entries.push(`${key}: ${value}`);
|
|
673
|
+
}
|
|
674
|
+
return `{ ${entries.join(', ')} }`;
|
|
675
|
+
}
|
|
653
676
|
default:
|
|
654
677
|
return null;
|
|
655
678
|
}
|
|
@@ -716,8 +739,8 @@ function buildTestPayload(
|
|
|
716
739
|
if (!model) return null;
|
|
717
740
|
|
|
718
741
|
const fields = model.fields.filter((f) => f.required);
|
|
719
|
-
// Only use
|
|
720
|
-
const usableFields = fields.filter((f) => fixtureValueForType(f.type, f.name, model.name) !== null);
|
|
742
|
+
// Only use fields that we can generate deterministic values for (primitives, enums, and nested models)
|
|
743
|
+
const usableFields = fields.filter((f) => fixtureValueForType(f.type, f.name, model.name, modelMap) !== null);
|
|
721
744
|
|
|
722
745
|
// Only generate a typed payload when ALL required fields have fixture values.
|
|
723
746
|
// A partial payload missing required fields would fail TypeScript type checking.
|
|
@@ -727,11 +750,12 @@ function buildTestPayload(
|
|
|
727
750
|
const snakeEntries: string[] = [];
|
|
728
751
|
|
|
729
752
|
for (const field of usableFields) {
|
|
730
|
-
const
|
|
753
|
+
const camelValue = fixtureValueForType(field.type, field.name, model.name, modelMap)!;
|
|
754
|
+
const wireValue = fixtureValueForType(field.type, field.name, model.name, modelMap, true)!;
|
|
731
755
|
const camelKey = fieldName(field.name);
|
|
732
756
|
const snakeKey = wireFieldName(field.name);
|
|
733
|
-
camelEntries.push(`${camelKey}: ${
|
|
734
|
-
snakeEntries.push(`${snakeKey}: ${
|
|
757
|
+
camelEntries.push(`${camelKey}: ${camelValue}`);
|
|
758
|
+
snakeEntries.push(`${snakeKey}: ${wireValue}`);
|
|
735
759
|
}
|
|
736
760
|
|
|
737
761
|
return {
|
|
@@ -775,7 +799,7 @@ function generateSerializerTests(spec: ApiSpec, ctx: EmitterContext): GeneratedF
|
|
|
775
799
|
serviceNameMap.set(service.name, resolveResourceClassName(service, ctx));
|
|
776
800
|
}
|
|
777
801
|
const resolveDir = (irService: string | undefined) =>
|
|
778
|
-
irService ?
|
|
802
|
+
irService ? resolveServiceDir(serviceNameMap.get(irService) ?? irService) : 'common';
|
|
779
803
|
|
|
780
804
|
// Only generate round-trip tests for models with fields that have serializers generated.
|
|
781
805
|
// Skip list metadata and list wrapper models since their serializers are not emitted.
|
package/src/node/utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Model, EmitterContext, Service, Operation, Field } from '@workos/oagen';
|
|
2
|
+
import { toPascalCase } from '@workos/oagen';
|
|
2
3
|
export {
|
|
3
4
|
collectModelRefs,
|
|
4
5
|
collectEnumRefs,
|
|
@@ -7,7 +8,14 @@ export {
|
|
|
7
8
|
collectRequestBodyModels,
|
|
8
9
|
} from '@workos/oagen';
|
|
9
10
|
import { mapTypeRef } from './type-map.js';
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
resolveInterfaceName,
|
|
13
|
+
fieldName,
|
|
14
|
+
resolveServiceDir,
|
|
15
|
+
resolveMethodName,
|
|
16
|
+
buildServiceNameMap,
|
|
17
|
+
SERVICE_COVERED_BY,
|
|
18
|
+
} from './naming.js';
|
|
11
19
|
import { assignModelsToServices } from '@workos/oagen';
|
|
12
20
|
|
|
13
21
|
/**
|
|
@@ -218,7 +226,7 @@ export function createServiceDirResolver(
|
|
|
218
226
|
const modelToService = assignModelsToServices(models, services);
|
|
219
227
|
const serviceNameMap = buildServiceNameMap(services, ctx);
|
|
220
228
|
const resolveDir = (irService: string | undefined) =>
|
|
221
|
-
irService ?
|
|
229
|
+
irService ? resolveServiceDir(serviceNameMap.get(irService) ?? irService) : 'common';
|
|
222
230
|
return { modelToService, serviceNameMap, resolveDir };
|
|
223
231
|
}
|
|
224
232
|
|
|
@@ -384,6 +392,9 @@ export function buildDeduplicationMap(models: Model[], ctx?: EmitterContext): Ma
|
|
|
384
392
|
* endpoints (e.g., `GET /connections`).
|
|
385
393
|
*/
|
|
386
394
|
export function isServiceCoveredByExisting(service: Service, ctx: EmitterContext): boolean {
|
|
395
|
+
// Explicit override: services known to be covered by existing hand-written classes
|
|
396
|
+
if (SERVICE_COVERED_BY[toPascalCase(service.name)]) return true;
|
|
397
|
+
|
|
387
398
|
const overlay = ctx.overlayLookup?.methodByOperation;
|
|
388
399
|
if (!overlay || overlay.size === 0) return false;
|
|
389
400
|
if (service.operations.length === 0) return false;
|
|
@@ -405,6 +416,45 @@ export function isServiceCoveredByExisting(service: Service, ctx: EmitterContext
|
|
|
405
416
|
});
|
|
406
417
|
}
|
|
407
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Check whether a fully-covered service has operations whose overlay-mapped
|
|
421
|
+
* methods are missing from the baseline class. Returns true when at least
|
|
422
|
+
* one operation maps to a method name that the baseline class does not have,
|
|
423
|
+
* meaning the merger needs to add new methods (skipIfExists must be removed).
|
|
424
|
+
*/
|
|
425
|
+
export function hasMethodsAbsentFromBaseline(service: Service, ctx: EmitterContext): boolean {
|
|
426
|
+
const baselineClasses = ctx.apiSurface?.classes;
|
|
427
|
+
if (!baselineClasses) return false;
|
|
428
|
+
|
|
429
|
+
// For services explicitly mapped to an existing class via SERVICE_COVERED_BY,
|
|
430
|
+
// check each operation's resolved method name against the target class directly.
|
|
431
|
+
// This avoids the overlay gap where new endpoints are silently skipped.
|
|
432
|
+
const targetClassName = SERVICE_COVERED_BY[toPascalCase(service.name)];
|
|
433
|
+
if (targetClassName) {
|
|
434
|
+
const cls = baselineClasses[targetClassName];
|
|
435
|
+
if (!cls) return true; // Target class missing from baseline — treat as absent
|
|
436
|
+
for (const op of service.operations) {
|
|
437
|
+
const method = resolveMethodName(op, service, ctx);
|
|
438
|
+
if (!cls.methods?.[method]) return true;
|
|
439
|
+
}
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Default overlay-based detection
|
|
444
|
+
const overlay = ctx.overlayLookup?.methodByOperation;
|
|
445
|
+
if (!overlay) return false;
|
|
446
|
+
|
|
447
|
+
for (const op of service.operations) {
|
|
448
|
+
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
449
|
+
const match = overlay.get(httpKey);
|
|
450
|
+
if (!match) continue;
|
|
451
|
+
const cls = baselineClasses[match.className];
|
|
452
|
+
if (!cls) continue;
|
|
453
|
+
if (!cls.methods?.[match.methodName]) return true;
|
|
454
|
+
}
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
|
|
408
458
|
/**
|
|
409
459
|
* Return operations in a service that are NOT covered by existing hand-written
|
|
410
460
|
* service classes. For fully uncovered services, returns all operations.
|