@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.
Files changed (44) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/.github/workflows/lint-pr-title.yml +1 -1
  3. package/.github/workflows/lint.yml +1 -1
  4. package/.github/workflows/release-please.yml +2 -2
  5. package/.github/workflows/release.yml +1 -1
  6. package/.node-version +1 -1
  7. package/.release-please-manifest.json +1 -1
  8. package/CHANGELOG.md +7 -0
  9. package/dist/index.d.mts.map +1 -1
  10. package/dist/index.mjs +1 -1
  11. package/dist/{plugin-CmfzawTp.mjs → plugin-eCuvoL1T.mjs} +2508 -1474
  12. package/dist/plugin-eCuvoL1T.mjs.map +1 -0
  13. package/dist/plugin.mjs +1 -1
  14. package/package.json +6 -6
  15. package/renovate.json +46 -6
  16. package/src/node/client.ts +19 -32
  17. package/src/node/enums.ts +67 -30
  18. package/src/node/errors.ts +2 -8
  19. package/src/node/field-plan.ts +188 -52
  20. package/src/node/fixtures.ts +11 -33
  21. package/src/node/index.ts +345 -20
  22. package/src/node/live-surface.ts +378 -0
  23. package/src/node/models.ts +540 -351
  24. package/src/node/naming.ts +119 -25
  25. package/src/node/node-overrides.ts +77 -0
  26. package/src/node/options.ts +41 -0
  27. package/src/node/resources.ts +455 -46
  28. package/src/node/sdk-errors.ts +0 -16
  29. package/src/node/tests.ts +108 -83
  30. package/src/node/type-map.ts +40 -18
  31. package/src/node/utils.ts +89 -102
  32. package/src/node/wrappers.ts +0 -20
  33. package/test/node/client.test.ts +106 -1201
  34. package/test/node/enums.test.ts +59 -130
  35. package/test/node/errors.test.ts +2 -3
  36. package/test/node/live-surface.test.ts +240 -0
  37. package/test/node/models.test.ts +396 -765
  38. package/test/node/naming.test.ts +69 -234
  39. package/test/node/resources.test.ts +376 -2036
  40. package/test/node/tests.test.ts +119 -0
  41. package/test/node/type-map.test.ts +49 -54
  42. package/test/node/utils.test.ts +29 -80
  43. package/dist/plugin-CmfzawTp.mjs.map +0 -1
  44. package/test/node/serializers.test.ts +0 -444
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { execFileSync } from 'node:child_process';
6
+ import type { ApiSpec, EmitterContext, Service, Model } from '@workos/oagen';
7
+ import { defaultSdkBehavior } from '@workos/oagen';
8
+ import { nodeEmitter } from '../../src/node/index.js';
9
+
10
+ const groupModel: Model = {
11
+ name: 'Group',
12
+ fields: [
13
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
14
+ { name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
15
+ ],
16
+ };
17
+
18
+ const groupService: Service = {
19
+ name: 'Groups',
20
+ operations: [
21
+ {
22
+ name: 'getGroup',
23
+ httpMethod: 'get',
24
+ path: '/organizations/{organizationId}/groups/{groupId}',
25
+ pathParams: [
26
+ { name: 'organizationId', type: { kind: 'primitive', type: 'string' }, required: true },
27
+ { name: 'groupId', type: { kind: 'primitive', type: 'string' }, required: true },
28
+ ],
29
+ queryParams: [],
30
+ headerParams: [],
31
+ response: { kind: 'model', name: 'Group' },
32
+ errors: [],
33
+ injectIdempotencyKey: false,
34
+ },
35
+ ],
36
+ };
37
+
38
+ const spec: ApiSpec = {
39
+ name: 'Test',
40
+ version: '1.0.0',
41
+ baseUrl: '',
42
+ services: [groupService],
43
+ models: [groupModel],
44
+ enums: [],
45
+ sdk: defaultSdkBehavior(),
46
+ };
47
+
48
+ const ctx: EmitterContext = {
49
+ namespace: 'workos',
50
+ namespacePascal: 'WorkOS',
51
+ spec,
52
+ };
53
+
54
+ function createTrackedSdkRoot(withHandTests = false): string {
55
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'node-owned-tests-'));
56
+ fs.mkdirSync(path.join(tmpRoot, 'src', 'groups'), { recursive: true });
57
+ fs.mkdirSync(path.join(tmpRoot, 'src', 'groups', 'fixtures'), { recursive: true });
58
+ fs.writeFileSync(path.join(tmpRoot, 'src', 'workos.ts'), '// @oagen-ignore-file\nexport class WorkOS {}\n');
59
+ if (withHandTests) {
60
+ fs.writeFileSync(path.join(tmpRoot, 'src', 'groups', 'groups.spec.ts'), "describe('old', () => {});\n");
61
+ fs.writeFileSync(path.join(tmpRoot, 'src', 'groups', 'fixtures', 'group.json'), '{"id":"old"}\n');
62
+ }
63
+ execFileSync('git', ['init'], { cwd: tmpRoot, stdio: 'ignore' });
64
+ execFileSync('git', ['add', 'src'], { cwd: tmpRoot, stdio: 'ignore' });
65
+ return tmpRoot;
66
+ }
67
+
68
+ describe('node test generation ownership', () => {
69
+ it('regenerates tests and fixtures for owned services', () => {
70
+ const tmpRoot = createTrackedSdkRoot();
71
+ try {
72
+ const result = nodeEmitter.generateTests!(spec, {
73
+ ...ctx,
74
+ outputDir: tmpRoot,
75
+ emitterOptions: { ownedServices: ['Groups'], regenerateOwnedTests: true },
76
+ } as EmitterContext);
77
+
78
+ const testFile = result.find((f) => f.path === 'src/groups/groups.spec.ts');
79
+ const fixtureFile = result.find((f) => f.path === 'src/groups/fixtures/group.json');
80
+ expect(testFile).toBeDefined();
81
+ expect(testFile!.overwriteExisting).toBe(true);
82
+ expect(fixtureFile).toBeDefined();
83
+ expect(fixtureFile!.overwriteExisting).toBe(true);
84
+ } finally {
85
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
86
+ }
87
+ });
88
+
89
+ it('preserves existing hand-written tests and fixtures for owned services', () => {
90
+ const tmpRoot = createTrackedSdkRoot(true);
91
+ try {
92
+ const result = nodeEmitter.generateTests!(spec, {
93
+ ...ctx,
94
+ outputDir: tmpRoot,
95
+ emitterOptions: { ownedServices: ['Groups'], regenerateOwnedTests: true },
96
+ } as EmitterContext);
97
+
98
+ expect(result.some((f) => f.path === 'src/groups/groups.spec.ts')).toBe(false);
99
+ expect(result.some((f) => f.path === 'src/groups/fixtures/group.json')).toBe(false);
100
+ } finally {
101
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
102
+ }
103
+ });
104
+
105
+ it('skips tests and fixtures when the service is not owned', () => {
106
+ const tmpRoot = createTrackedSdkRoot();
107
+ try {
108
+ const result = nodeEmitter.generateTests!(spec, {
109
+ ...ctx,
110
+ outputDir: tmpRoot,
111
+ emitterOptions: { regenerateOwnedTests: true },
112
+ } as EmitterContext);
113
+
114
+ expect(result.some((f) => f.path.startsWith('src/groups/'))).toBe(false);
115
+ } finally {
116
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
117
+ }
118
+ });
119
+ });
@@ -3,49 +3,53 @@ import { mapTypeRef, mapWireTypeRef } from '../../src/node/type-map.js';
3
3
  import type { TypeRef } from '@workos/oagen';
4
4
 
5
5
  describe('mapTypeRef', () => {
6
- it('maps primitive string', () => {
6
+ it('maps string primitive', () => {
7
7
  const ref: TypeRef = { kind: 'primitive', type: 'string' };
8
8
  expect(mapTypeRef(ref)).toBe('string');
9
9
  });
10
10
 
11
- it('maps primitive integer to number', () => {
11
+ it('maps integer primitive', () => {
12
12
  const ref: TypeRef = { kind: 'primitive', type: 'integer' };
13
13
  expect(mapTypeRef(ref)).toBe('number');
14
14
  });
15
15
 
16
- it('maps primitive boolean', () => {
16
+ it('maps boolean primitive', () => {
17
17
  const ref: TypeRef = { kind: 'primitive', type: 'boolean' };
18
18
  expect(mapTypeRef(ref)).toBe('boolean');
19
19
  });
20
20
 
21
- it('maps unknown to any', () => {
21
+ it('maps unknown primitive to any', () => {
22
22
  const ref: TypeRef = { kind: 'primitive', type: 'unknown' };
23
23
  expect(mapTypeRef(ref)).toBe('any');
24
24
  });
25
25
 
26
- it('maps array of strings', () => {
27
- const ref: TypeRef = {
28
- kind: 'array',
29
- items: { kind: 'primitive', type: 'string' },
30
- };
31
- expect(mapTypeRef(ref)).toBe('string[]');
26
+ it('maps date-time to Date', () => {
27
+ const ref: TypeRef = { kind: 'primitive', type: 'string', format: 'date-time' };
28
+ expect(mapTypeRef(ref)).toBe('Date');
32
29
  });
33
30
 
34
- it('maps model reference', () => {
31
+ it('maps model ref to model name', () => {
35
32
  const ref: TypeRef = { kind: 'model', name: 'Organization' };
36
33
  expect(mapTypeRef(ref)).toBe('Organization');
37
34
  });
38
35
 
39
- it('maps enum reference', () => {
40
- const ref: TypeRef = { kind: 'enum', name: 'Status' };
36
+ it('maps enum ref to enum name', () => {
37
+ const ref: TypeRef = { kind: 'enum', name: 'Status', values: ['active', 'inactive'] };
41
38
  expect(mapTypeRef(ref)).toBe('Status');
42
39
  });
43
40
 
41
+ it('maps array of primitives', () => {
42
+ const ref: TypeRef = { kind: 'array', items: { kind: 'primitive', type: 'string' } };
43
+ expect(mapTypeRef(ref)).toBe('string[]');
44
+ });
45
+
46
+ it('maps array of models', () => {
47
+ const ref: TypeRef = { kind: 'array', items: { kind: 'model', name: 'Org' } };
48
+ expect(mapTypeRef(ref)).toBe('Org[]');
49
+ });
50
+
44
51
  it('maps nullable type', () => {
45
- const ref: TypeRef = {
46
- kind: 'nullable',
47
- inner: { kind: 'primitive', type: 'string' },
48
- };
52
+ const ref: TypeRef = { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } };
49
53
  expect(mapTypeRef(ref)).toBe('string | null');
50
54
  });
51
55
 
@@ -64,32 +68,14 @@ describe('mapTypeRef', () => {
64
68
  const ref: TypeRef = {
65
69
  kind: 'union',
66
70
  variants: [
67
- { kind: 'model', name: 'AuthenticationFactorTotp' },
68
- { kind: 'model', name: 'AuthenticationFactorTotp' },
71
+ { kind: 'primitive', type: 'string' },
72
+ { kind: 'primitive', type: 'string' },
69
73
  ],
70
74
  };
71
- expect(mapTypeRef(ref)).toBe('AuthenticationFactorTotp');
72
- });
73
-
74
- it('maps map type', () => {
75
- const ref: TypeRef = {
76
- kind: 'map',
77
- valueType: { kind: 'primitive', type: 'string' },
78
- };
79
- expect(mapTypeRef(ref)).toBe('Record<string, string>');
80
- });
81
-
82
- it('maps literal string', () => {
83
- const ref: TypeRef = { kind: 'literal', value: 'organization' };
84
- expect(mapTypeRef(ref)).toBe("'organization'");
85
- });
86
-
87
- it('maps literal number', () => {
88
- const ref: TypeRef = { kind: 'literal', value: 42 };
89
- expect(mapTypeRef(ref)).toBe('42');
75
+ expect(mapTypeRef(ref)).toBe('string');
90
76
  });
91
77
 
92
- it('parenthesizes union in array', () => {
78
+ it('parenthesizes unions in arrays', () => {
93
79
  const ref: TypeRef = {
94
80
  kind: 'array',
95
81
  items: {
@@ -102,37 +88,46 @@ describe('mapTypeRef', () => {
102
88
  };
103
89
  expect(mapTypeRef(ref)).toBe('(string | number)[]');
104
90
  });
91
+
92
+ it('maps map type', () => {
93
+ const ref: TypeRef = { kind: 'map', valueType: { kind: 'primitive', type: 'string' } };
94
+ expect(mapTypeRef(ref)).toBe('Record<string, string>');
95
+ });
96
+
97
+ it('maps string literal', () => {
98
+ const ref: TypeRef = { kind: 'literal', value: 'active' };
99
+ expect(mapTypeRef(ref)).toBe("'active'");
100
+ });
101
+
102
+ it('maps number literal', () => {
103
+ const ref: TypeRef = { kind: 'literal', value: 42 };
104
+ expect(mapTypeRef(ref)).toBe('42');
105
+ });
105
106
  });
106
107
 
107
108
  describe('mapWireTypeRef', () => {
108
- it('maps model reference with Response suffix', () => {
109
+ it('maps model ref with Response suffix', () => {
109
110
  const ref: TypeRef = { kind: 'model', name: 'Organization' };
110
111
  expect(mapWireTypeRef(ref)).toBe('OrganizationResponse');
111
112
  });
112
113
 
113
114
  it('maps array of models with Response suffix', () => {
114
- const ref: TypeRef = {
115
- kind: 'array',
116
- items: { kind: 'model', name: 'OrganizationDomain' },
117
- };
118
- expect(mapWireTypeRef(ref)).toBe('OrganizationDomainResponse[]');
115
+ const ref: TypeRef = { kind: 'array', items: { kind: 'model', name: 'Org' } };
116
+ expect(mapWireTypeRef(ref)).toBe('OrgResponse[]');
119
117
  });
120
118
 
121
- it('keeps primitives unchanged', () => {
122
- const ref: TypeRef = { kind: 'primitive', type: 'string' };
119
+ it('maps date-time as string in wire type', () => {
120
+ const ref: TypeRef = { kind: 'primitive', type: 'string', format: 'date-time' };
123
121
  expect(mapWireTypeRef(ref)).toBe('string');
124
122
  });
125
123
 
126
- it('keeps enum references unchanged', () => {
127
- const ref: TypeRef = { kind: 'enum', name: 'Status' };
124
+ it('maps enum ref unchanged', () => {
125
+ const ref: TypeRef = { kind: 'enum', name: 'Status', values: ['active'] };
128
126
  expect(mapWireTypeRef(ref)).toBe('Status');
129
127
  });
130
128
 
131
129
  it('maps nullable model with Response suffix', () => {
132
- const ref: TypeRef = {
133
- kind: 'nullable',
134
- inner: { kind: 'model', name: 'Organization' },
135
- };
136
- expect(mapWireTypeRef(ref)).toBe('OrganizationResponse | null');
130
+ const ref: TypeRef = { kind: 'nullable', inner: { kind: 'model', name: 'Org' } };
131
+ expect(mapWireTypeRef(ref)).toBe('OrgResponse | null');
137
132
  });
138
133
  });
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { modelHasNewFields } from '../../src/node/utils.js';
3
2
  import type { EmitterContext, ApiSpec, Model } from '@workos/oagen';
4
3
  import { defaultSdkBehavior } from '@workos/oagen';
4
+ import { modelHasNewFields } from '../../src/node/utils.js';
5
5
 
6
6
  const emptySpec: ApiSpec = {
7
7
  name: 'Test',
@@ -20,121 +20,70 @@ const ctx: EmitterContext = {
20
20
  };
21
21
 
22
22
  describe('modelHasNewFields', () => {
23
- const model: Model = {
24
- name: 'Organization',
25
- fields: [
26
- { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
27
- { name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
28
- ],
29
- };
30
-
31
- it('returns true when no apiSurface exists (Scenario B)', () => {
23
+ it('returns true when no apiSurface (Scenario B)', () => {
24
+ const model: Model = {
25
+ name: 'Organization',
26
+ fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
27
+ };
32
28
  expect(modelHasNewFields(model, ctx)).toBe(true);
33
29
  });
34
30
 
35
- it('returns true when model has no baseline entry (new model)', () => {
31
+ it('returns true when model not in baseline', () => {
32
+ const model: Model = {
33
+ name: 'NewModel',
34
+ fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
35
+ };
36
36
  const ctxWithSurface: EmitterContext = {
37
37
  ...ctx,
38
- apiSurface: {
39
- language: 'node',
40
- extractedFrom: 'test',
41
- extractedAt: '2024-01-01',
42
- classes: {},
43
- interfaces: {},
44
- typeAliases: {},
45
- enums: {},
46
- exports: {},
47
- },
38
+ apiSurface: { interfaces: { Organization: { fields: {} } } } as any,
48
39
  };
49
40
  expect(modelHasNewFields(model, ctxWithSurface)).toBe(true);
50
41
  });
51
42
 
52
- it('returns false when all fields exist in baseline', () => {
53
- const ctxWithBaseline: EmitterContext = {
54
- ...ctx,
55
- apiSurface: {
56
- language: 'node',
57
- extractedFrom: 'test',
58
- extractedAt: '2024-01-01',
59
- classes: {},
60
- typeAliases: {},
61
- enums: {},
62
- exports: {},
63
- interfaces: {
64
- Organization: {
65
- name: 'Organization',
66
- fields: {
67
- id: { name: 'id', type: 'string', optional: false },
68
- name: { name: 'name', type: 'string', optional: false },
69
- },
70
- extends: [],
71
- },
72
- },
73
- },
74
- };
75
- expect(modelHasNewFields(model, ctxWithBaseline)).toBe(false);
76
- });
77
-
78
- it('returns true when model has one new field not in baseline', () => {
79
- const modelWithNewField: Model = {
43
+ it('returns false when all fields in baseline', () => {
44
+ const model: Model = {
80
45
  name: 'Organization',
81
46
  fields: [
82
47
  { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
83
48
  { name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
84
- { name: 'slug', type: { kind: 'primitive', type: 'string' }, required: false },
85
49
  ],
86
50
  };
87
- const ctxWithBaseline: EmitterContext = {
51
+ const ctxWithSurface: EmitterContext = {
88
52
  ...ctx,
89
53
  apiSurface: {
90
- language: 'node',
91
- extractedFrom: 'test',
92
- extractedAt: '2024-01-01',
93
- classes: {},
94
- typeAliases: {},
95
- enums: {},
96
- exports: {},
97
54
  interfaces: {
98
55
  Organization: {
99
- name: 'Organization',
100
56
  fields: {
101
- id: { name: 'id', type: 'string', optional: false },
102
- name: { name: 'name', type: 'string', optional: false },
57
+ id: { type: 'string', optional: false },
58
+ name: { type: 'string', optional: false },
103
59
  },
104
- extends: [],
105
60
  },
106
61
  },
107
- },
62
+ } as any,
108
63
  };
109
- expect(modelHasNewFields(modelWithNewField, ctxWithBaseline)).toBe(true);
64
+ expect(modelHasNewFields(model, ctxWithSurface)).toBe(false);
110
65
  });
111
66
 
112
- it('converts snake_case IR field names to camelCase for baseline comparison', () => {
113
- const snakeModel: Model = {
67
+ it('returns true when new field added', () => {
68
+ const model: Model = {
114
69
  name: 'Organization',
115
- fields: [{ name: 'organization_id', type: { kind: 'primitive', type: 'string' }, required: true }],
70
+ fields: [
71
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
72
+ { name: 'new_field', type: { kind: 'primitive', type: 'string' }, required: false },
73
+ ],
116
74
  };
117
- const ctxWithBaseline: EmitterContext = {
75
+ const ctxWithSurface: EmitterContext = {
118
76
  ...ctx,
119
77
  apiSurface: {
120
- language: 'node',
121
- extractedFrom: 'test',
122
- extractedAt: '2024-01-01',
123
- classes: {},
124
- typeAliases: {},
125
- enums: {},
126
- exports: {},
127
78
  interfaces: {
128
79
  Organization: {
129
- name: 'Organization',
130
80
  fields: {
131
- organizationId: { name: 'organizationId', type: 'string', optional: false },
81
+ id: { type: 'string', optional: false },
132
82
  },
133
- extends: [],
134
83
  },
135
84
  },
136
- },
85
+ } as any,
137
86
  };
138
- expect(modelHasNewFields(snakeModel, ctxWithBaseline)).toBe(false);
87
+ expect(modelHasNewFields(model, ctxWithSurface)).toBe(true);
139
88
  });
140
89
  });