@workos/oagen-emitters 0.14.1 → 0.14.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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +9 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-DRGwxN88.mjs → plugin-BbSmT2kj.mjs} +125 -40
- package/dist/plugin-BbSmT2kj.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +1 -1
- package/src/go/fixtures.ts +6 -2
- package/src/go/models.ts +8 -2
- package/src/kotlin/models.ts +10 -4
- package/src/kotlin/resources.ts +17 -2
- package/src/node/fixtures.ts +13 -3
- package/src/node/index.ts +14 -0
- package/src/node/models.ts +50 -30
- package/src/node/naming.ts +25 -1
- package/src/node/resources.ts +9 -1
- package/src/node/tests.ts +8 -1
- package/src/node/utils.ts +1 -0
- package/src/python/fixtures.ts +6 -2
- package/src/python/models.ts +6 -1
- package/src/python/tests.ts +7 -1
- package/src/ruby/parameter-groups.ts +4 -2
- package/src/ruby/rbi.ts +1 -0
- package/src/shared/model-utils.ts +36 -1
- package/test/node/naming.test.ts +45 -1
- package/test/node/tests.test.ts +69 -0
- package/test/shared/model-utils.test.ts +97 -0
- package/dist/plugin-DRGwxN88.mjs.map +0 -1
package/test/node/tests.test.ts
CHANGED
|
@@ -268,4 +268,73 @@ describe('node test generation ownership', () => {
|
|
|
268
268
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
269
269
|
}
|
|
270
270
|
});
|
|
271
|
+
|
|
272
|
+
it('asserts toBeNull() for nullable fields whose example is null', () => {
|
|
273
|
+
// When the spec gives `example: null` on a nullable field — common for
|
|
274
|
+
// optional date-times like `last_used_at` — the previous emitter would
|
|
275
|
+
// emit `expect(x.toISOString()).toBe(null)`, which both blows up at
|
|
276
|
+
// runtime on a null `x` and never matches when `x` is a Date.
|
|
277
|
+
const secretModel: Model = {
|
|
278
|
+
name: 'Secret',
|
|
279
|
+
fields: [
|
|
280
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true, example: 'sec_1' },
|
|
281
|
+
{
|
|
282
|
+
name: 'last_used_at',
|
|
283
|
+
type: {
|
|
284
|
+
kind: 'nullable',
|
|
285
|
+
inner: { kind: 'primitive', type: 'string', format: 'date-time' },
|
|
286
|
+
},
|
|
287
|
+
required: true,
|
|
288
|
+
example: null,
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: 'created_at',
|
|
292
|
+
type: { kind: 'primitive', type: 'string', format: 'date-time' },
|
|
293
|
+
required: true,
|
|
294
|
+
example: '2026-01-15T12:00:00.000Z',
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const showOp = {
|
|
300
|
+
name: 'showSecret',
|
|
301
|
+
httpMethod: 'get' as const,
|
|
302
|
+
path: '/secrets/{id}',
|
|
303
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive' as const, type: 'string' as const }, required: true }],
|
|
304
|
+
queryParams: [],
|
|
305
|
+
headerParams: [],
|
|
306
|
+
response: { kind: 'model' as const, name: 'Secret' },
|
|
307
|
+
errors: [],
|
|
308
|
+
injectIdempotencyKey: false,
|
|
309
|
+
};
|
|
310
|
+
const secretService: Service = { name: 'Secrets', operations: [showOp] };
|
|
311
|
+
const secretSpec: ApiSpec = {
|
|
312
|
+
...spec,
|
|
313
|
+
models: [secretModel],
|
|
314
|
+
services: [secretService],
|
|
315
|
+
};
|
|
316
|
+
const tmpRoot = createTrackedSdkRoot();
|
|
317
|
+
try {
|
|
318
|
+
fs.mkdirSync(path.join(tmpRoot, 'src', 'secrets', 'fixtures'), { recursive: true });
|
|
319
|
+
execFileSync('git', ['add', 'src'], { cwd: tmpRoot, stdio: 'ignore' });
|
|
320
|
+
|
|
321
|
+
const result = nodeEmitter.generateTests!(secretSpec, {
|
|
322
|
+
...ctx,
|
|
323
|
+
spec: secretSpec,
|
|
324
|
+
outputDir: tmpRoot,
|
|
325
|
+
emitterOptions: { ownedServices: ['Secrets'], regenerateOwnedTests: true },
|
|
326
|
+
} as EmitterContext);
|
|
327
|
+
|
|
328
|
+
const testFile = result.find((f) => f.path === 'src/secrets/secrets.spec.ts');
|
|
329
|
+
expect(testFile).toBeDefined();
|
|
330
|
+
const content = testFile!.content;
|
|
331
|
+
// Null example on a nullable date-time → toBeNull(), not `.toISOString().toBe(null)`.
|
|
332
|
+
expect(content).toContain('expect(result.lastUsedAt).toBeNull();');
|
|
333
|
+
expect(content).not.toContain('lastUsedAt.toISOString()).toBe(null)');
|
|
334
|
+
// Non-null date-time examples still go through `.toISOString()`.
|
|
335
|
+
expect(content).toContain("expect(result.createdAt.toISOString()).toBe('2026-01-15T12:00:00.000Z');");
|
|
336
|
+
} finally {
|
|
337
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
338
|
+
}
|
|
339
|
+
});
|
|
271
340
|
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { Model } from '@workos/oagen';
|
|
3
|
+
import {
|
|
4
|
+
collectReferencedListMetadataModels,
|
|
5
|
+
isListMetadataModel,
|
|
6
|
+
isListWrapperModel,
|
|
7
|
+
} from '../../src/shared/model-utils.js';
|
|
8
|
+
|
|
9
|
+
const listMetadataModel: Model = {
|
|
10
|
+
name: 'ListMetadata',
|
|
11
|
+
fields: [
|
|
12
|
+
{
|
|
13
|
+
name: 'before',
|
|
14
|
+
type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } },
|
|
15
|
+
required: false,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'after',
|
|
19
|
+
type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } },
|
|
20
|
+
required: false,
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe('isListMetadataModel', () => {
|
|
26
|
+
it('matches a two-field nullable-string before/after shape', () => {
|
|
27
|
+
expect(isListMetadataModel(listMetadataModel)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('collectReferencedListMetadataModels', () => {
|
|
32
|
+
it('returns nothing when no surviving wrapper references the model', () => {
|
|
33
|
+
// A paginated list wrapper that the SDK pagination machinery unwraps —
|
|
34
|
+
// not in `nonPaginatedRefs`, so it counts as skipped.
|
|
35
|
+
const paginatedWrapper: Model = {
|
|
36
|
+
name: 'OrgList',
|
|
37
|
+
fields: [
|
|
38
|
+
{ name: 'data', type: { kind: 'array', items: { kind: 'model', name: 'Org' } }, required: true },
|
|
39
|
+
{ name: 'list_metadata', type: { kind: 'model', name: 'ListMetadata' }, required: true },
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
const result = collectReferencedListMetadataModels([listMetadataModel, paginatedWrapper], new Set());
|
|
43
|
+
expect(result.size).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('flags the ListMetadata model when a non-paginated wrapper still references it', () => {
|
|
47
|
+
// `VersionListResponse` is shaped like a list envelope but the operation
|
|
48
|
+
// has no pagination params, so it survives the wrapper skip — and its
|
|
49
|
+
// `list_metadata` field needs the `ListMetadata` interface on disk.
|
|
50
|
+
const versionWrapper: Model = {
|
|
51
|
+
name: 'VersionListResponse',
|
|
52
|
+
fields: [
|
|
53
|
+
{ name: 'data', type: { kind: 'array', items: { kind: 'model', name: 'Version' } }, required: true },
|
|
54
|
+
{ name: 'list_metadata', type: { kind: 'model', name: 'ListMetadata' }, required: true },
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
const result = collectReferencedListMetadataModels(
|
|
58
|
+
[listMetadataModel, versionWrapper],
|
|
59
|
+
new Set(['VersionListResponse']),
|
|
60
|
+
);
|
|
61
|
+
expect(result.has('ListMetadata')).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('flags the ListMetadata model when a non-wrapper IR model references it directly', () => {
|
|
65
|
+
// Defensive: should anything else point at a `ListMetadata`-shape model
|
|
66
|
+
// as a regular field, we still need the file.
|
|
67
|
+
const customModel: Model = {
|
|
68
|
+
name: 'Custom',
|
|
69
|
+
fields: [{ name: 'meta', type: { kind: 'model', name: 'ListMetadata' }, required: true }],
|
|
70
|
+
};
|
|
71
|
+
const result = collectReferencedListMetadataModels([listMetadataModel, customModel], new Set());
|
|
72
|
+
expect(result.has('ListMetadata')).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns an empty set when no ListMetadata-shape model exists in the IR', () => {
|
|
76
|
+
const regular: Model = {
|
|
77
|
+
name: 'Foo',
|
|
78
|
+
fields: [{ name: 'bar', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
79
|
+
};
|
|
80
|
+
const result = collectReferencedListMetadataModels([regular], new Set());
|
|
81
|
+
expect(result.size).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('isListWrapperModel + isListMetadataModel — sanity', () => {
|
|
86
|
+
it('does not classify a wrapper as a metadata model', () => {
|
|
87
|
+
const wrapper: Model = {
|
|
88
|
+
name: 'OrgList',
|
|
89
|
+
fields: [
|
|
90
|
+
{ name: 'data', type: { kind: 'array', items: { kind: 'model', name: 'Org' } }, required: true },
|
|
91
|
+
{ name: 'list_metadata', type: { kind: 'model', name: 'ListMetadata' }, required: true },
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
expect(isListWrapperModel(wrapper)).toBe(true);
|
|
95
|
+
expect(isListMetadataModel(wrapper)).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|