@workos/oagen-emitters 0.0.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/.github/workflows/ci.yml +20 -0
- package/.github/workflows/lint-pr-title.yml +16 -0
- package/.github/workflows/lint.yml +21 -0
- package/.github/workflows/release-please.yml +28 -0
- package/.github/workflows/release.yml +32 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.husky/pre-push +1 -0
- package/.node-version +1 -0
- package/.oxfmtrc.json +10 -0
- package/.oxlintrc.json +29 -0
- package/.vscode/settings.json +11 -0
- package/LICENSE.txt +21 -0
- package/README.md +123 -0
- package/commitlint.config.ts +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2158 -0
- package/docs/endpoint-coverage.md +275 -0
- package/docs/sdk-architecture/node.md +355 -0
- package/oagen.config.ts +51 -0
- package/package.json +83 -0
- package/renovate.json +26 -0
- package/smoke/sdk-dotnet.ts +903 -0
- package/smoke/sdk-elixir.ts +771 -0
- package/smoke/sdk-go.ts +948 -0
- package/smoke/sdk-kotlin.ts +799 -0
- package/smoke/sdk-node.ts +516 -0
- package/smoke/sdk-php.ts +699 -0
- package/smoke/sdk-python.ts +738 -0
- package/smoke/sdk-ruby.ts +723 -0
- package/smoke/sdk-rust.ts +774 -0
- package/src/compat/extractors/dotnet.ts +8 -0
- package/src/compat/extractors/elixir.ts +8 -0
- package/src/compat/extractors/go.ts +8 -0
- package/src/compat/extractors/kotlin.ts +8 -0
- package/src/compat/extractors/node.ts +8 -0
- package/src/compat/extractors/php.ts +8 -0
- package/src/compat/extractors/python.ts +8 -0
- package/src/compat/extractors/ruby.ts +8 -0
- package/src/compat/extractors/rust.ts +8 -0
- package/src/index.ts +1 -0
- package/src/node/client.ts +356 -0
- package/src/node/common.ts +203 -0
- package/src/node/config.ts +70 -0
- package/src/node/enums.ts +87 -0
- package/src/node/errors.ts +205 -0
- package/src/node/fixtures.ts +139 -0
- package/src/node/index.ts +57 -0
- package/src/node/manifest.ts +23 -0
- package/src/node/models.ts +323 -0
- package/src/node/naming.ts +96 -0
- package/src/node/resources.ts +380 -0
- package/src/node/serializers.ts +286 -0
- package/src/node/tests.ts +336 -0
- package/src/node/type-map.ts +56 -0
- package/src/node/utils.ts +164 -0
- package/test/compat/extractors/node.test.ts +145 -0
- package/test/fixtures/sample-sdk-node/package.json +7 -0
- package/test/fixtures/sample-sdk-node/src/client.ts +24 -0
- package/test/fixtures/sample-sdk-node/src/index.ts +4 -0
- package/test/fixtures/sample-sdk-node/src/models.ts +28 -0
- package/test/fixtures/sample-sdk-node/tsconfig.json +13 -0
- package/test/node/client.test.ts +165 -0
- package/test/node/enums.test.ts +128 -0
- package/test/node/errors.test.ts +65 -0
- package/test/node/models.test.ts +301 -0
- package/test/node/naming.test.ts +212 -0
- package/test/node/resources.test.ts +260 -0
- package/test/node/serializers.test.ts +206 -0
- package/test/node/type-map.test.ts +127 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +8 -0
- package/vitest.config.ts +4 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { GeneratedFile } from '@workos/oagen';
|
|
2
|
+
|
|
3
|
+
export function generateConfig(): GeneratedFile[] {
|
|
4
|
+
return [
|
|
5
|
+
{
|
|
6
|
+
path: 'src/common/interfaces/workos-options.interface.ts',
|
|
7
|
+
content: `export interface WorkOSOptions {
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
apiHostname?: string;
|
|
10
|
+
https?: boolean;
|
|
11
|
+
port?: number;
|
|
12
|
+
config?: RequestInit;
|
|
13
|
+
fetchFn?: typeof fetch;
|
|
14
|
+
clientId?: string;
|
|
15
|
+
timeout?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AppInfo {
|
|
19
|
+
name: string;
|
|
20
|
+
version?: string;
|
|
21
|
+
}`,
|
|
22
|
+
skipIfExists: true,
|
|
23
|
+
integrateTarget: false,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
path: 'src/common/interfaces/post-options.interface.ts',
|
|
27
|
+
content: `export interface PostOptions {
|
|
28
|
+
query?: Record<string, any>;
|
|
29
|
+
idempotencyKey?: string;
|
|
30
|
+
warrantToken?: string;
|
|
31
|
+
skipApiKeyCheck?: boolean;
|
|
32
|
+
}`,
|
|
33
|
+
skipIfExists: true,
|
|
34
|
+
integrateTarget: false,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
path: 'src/common/interfaces/get-options.interface.ts',
|
|
38
|
+
content: `export interface GetOptions {
|
|
39
|
+
query?: Record<string, any>;
|
|
40
|
+
accessToken?: string;
|
|
41
|
+
warrantToken?: string;
|
|
42
|
+
skipApiKeyCheck?: boolean;
|
|
43
|
+
}`,
|
|
44
|
+
skipIfExists: true,
|
|
45
|
+
integrateTarget: false,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
path: 'src/common/interfaces/pagination-options.interface.ts',
|
|
49
|
+
content: `export interface PaginationOptions {
|
|
50
|
+
limit?: number;
|
|
51
|
+
before?: string | null;
|
|
52
|
+
after?: string | null;
|
|
53
|
+
order?: 'asc' | 'desc';
|
|
54
|
+
}`,
|
|
55
|
+
skipIfExists: true,
|
|
56
|
+
integrateTarget: false,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
path: 'src/common/interfaces/request-exception.interface.ts',
|
|
60
|
+
content: `export interface RequestException {
|
|
61
|
+
readonly status: number;
|
|
62
|
+
readonly name: string;
|
|
63
|
+
readonly requestID: string;
|
|
64
|
+
readonly code?: string;
|
|
65
|
+
}`,
|
|
66
|
+
skipIfExists: true,
|
|
67
|
+
integrateTarget: false,
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Enum, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
|
|
2
|
+
import { walkTypeRef } from '@workos/oagen';
|
|
3
|
+
import { fileName, serviceDirName, buildServiceNameMap } from './naming.js';
|
|
4
|
+
import { docComment } from './utils.js';
|
|
5
|
+
|
|
6
|
+
export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
7
|
+
if (enums.length === 0) return [];
|
|
8
|
+
|
|
9
|
+
const enumToService = assignEnumsToServices(enums, ctx.spec.services);
|
|
10
|
+
const serviceNameMap = buildServiceNameMap(ctx.spec.services, ctx);
|
|
11
|
+
const resolveDir = (irService: string | undefined) =>
|
|
12
|
+
irService ? serviceDirName(serviceNameMap.get(irService) ?? irService) : 'common';
|
|
13
|
+
const files: GeneratedFile[] = [];
|
|
14
|
+
|
|
15
|
+
for (const enumDef of enums) {
|
|
16
|
+
const service = enumToService.get(enumDef.name);
|
|
17
|
+
const dirName = resolveDir(service);
|
|
18
|
+
|
|
19
|
+
// Check baseline surface for representation and values
|
|
20
|
+
const baselineEnum = ctx.apiSurface?.enums?.[enumDef.name];
|
|
21
|
+
const baselineAlias = ctx.apiSurface?.typeAliases?.[enumDef.name];
|
|
22
|
+
const lines: string[] = [];
|
|
23
|
+
|
|
24
|
+
if (baselineEnum?.members) {
|
|
25
|
+
// Generate TS `enum` using baseline member names and values directly
|
|
26
|
+
lines.push(`export enum ${enumDef.name} {`);
|
|
27
|
+
for (const [memberName, memberValue] of Object.entries(baselineEnum.members)) {
|
|
28
|
+
const valueStr = typeof memberValue === 'string' ? `'${memberValue}'` : String(memberValue);
|
|
29
|
+
lines.push(` ${memberName} = ${valueStr},`);
|
|
30
|
+
}
|
|
31
|
+
lines.push('}');
|
|
32
|
+
} else if (baselineAlias?.value) {
|
|
33
|
+
// Use the exact baseline type alias value for guaranteed compat match
|
|
34
|
+
lines.push(`export type ${enumDef.name} = ${baselineAlias.value};`);
|
|
35
|
+
} else {
|
|
36
|
+
// No baseline — generate string literal union from IR values
|
|
37
|
+
const values = enumDef.values;
|
|
38
|
+
lines.push(`export type ${enumDef.name} =`);
|
|
39
|
+
for (let i = 0; i < values.length; i++) {
|
|
40
|
+
const v = values[i];
|
|
41
|
+
const valueStr = typeof v.value === 'string' ? `'${v.value}'` : String(v.value);
|
|
42
|
+
if (v.description || v.deprecated) {
|
|
43
|
+
const parts: string[] = [];
|
|
44
|
+
if (v.description) parts.push(v.description);
|
|
45
|
+
if (v.deprecated) parts.push('@deprecated');
|
|
46
|
+
lines.push(...docComment(parts.join('\n'), 2));
|
|
47
|
+
}
|
|
48
|
+
const suffix = i === values.length - 1 ? ';' : '';
|
|
49
|
+
lines.push(` | ${valueStr}${suffix}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
files.push({
|
|
54
|
+
path: `src/${dirName}/interfaces/${fileName(enumDef.name)}.interface.ts`,
|
|
55
|
+
content: lines.join('\n'),
|
|
56
|
+
skipIfExists: true,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return files;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function assignEnumsToServices(enums: Enum[], services: Service[]): Map<string, string> {
|
|
64
|
+
const enumToService = new Map<string, string>();
|
|
65
|
+
const enumNames = new Set(enums.map((e) => e.name));
|
|
66
|
+
|
|
67
|
+
for (const service of services) {
|
|
68
|
+
for (const op of service.operations) {
|
|
69
|
+
const refs = new Set<string>();
|
|
70
|
+
const collect = (ref: any) => {
|
|
71
|
+
walkTypeRef(ref, { enum: (r: any) => refs.add(r.name) });
|
|
72
|
+
};
|
|
73
|
+
if (op.requestBody) collect(op.requestBody);
|
|
74
|
+
collect(op.response);
|
|
75
|
+
for (const p of [...op.pathParams, ...op.queryParams, ...op.headerParams]) {
|
|
76
|
+
collect(p.type);
|
|
77
|
+
}
|
|
78
|
+
for (const name of refs) {
|
|
79
|
+
if (enumNames.has(name) && !enumToService.has(name)) {
|
|
80
|
+
enumToService.set(name, service.name);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return enumToService;
|
|
87
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { GeneratedFile } from '@workos/oagen';
|
|
2
|
+
|
|
3
|
+
export function generateErrors(): GeneratedFile[] {
|
|
4
|
+
return [
|
|
5
|
+
{
|
|
6
|
+
path: 'src/common/exceptions/bad-request.exception.ts',
|
|
7
|
+
content: `export class BadRequestException extends Error {
|
|
8
|
+
readonly status = 400;
|
|
9
|
+
readonly name = 'BadRequestException';
|
|
10
|
+
readonly requestID: string;
|
|
11
|
+
readonly code?: string;
|
|
12
|
+
|
|
13
|
+
constructor({
|
|
14
|
+
code,
|
|
15
|
+
message,
|
|
16
|
+
requestID,
|
|
17
|
+
}: {
|
|
18
|
+
code?: string;
|
|
19
|
+
message?: string;
|
|
20
|
+
requestID: string;
|
|
21
|
+
}) {
|
|
22
|
+
super();
|
|
23
|
+
this.message = message ?? 'Bad request';
|
|
24
|
+
this.requestID = requestID;
|
|
25
|
+
if (code) this.code = code;
|
|
26
|
+
}
|
|
27
|
+
}`,
|
|
28
|
+
skipIfExists: true,
|
|
29
|
+
integrateTarget: false,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
path: 'src/common/exceptions/unauthorized.exception.ts',
|
|
33
|
+
content: `export class UnauthorizedException extends Error {
|
|
34
|
+
readonly status = 401;
|
|
35
|
+
readonly name = 'UnauthorizedException';
|
|
36
|
+
readonly requestID: string;
|
|
37
|
+
|
|
38
|
+
constructor(requestID: string) {
|
|
39
|
+
super();
|
|
40
|
+
this.message = 'Unauthorized';
|
|
41
|
+
this.requestID = requestID;
|
|
42
|
+
}
|
|
43
|
+
}`,
|
|
44
|
+
skipIfExists: true,
|
|
45
|
+
integrateTarget: false,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
path: 'src/common/exceptions/not-found.exception.ts',
|
|
49
|
+
content: `export class NotFoundException extends Error {
|
|
50
|
+
readonly status = 404;
|
|
51
|
+
readonly name = 'NotFoundException';
|
|
52
|
+
readonly requestID: string;
|
|
53
|
+
readonly code?: string;
|
|
54
|
+
|
|
55
|
+
constructor({
|
|
56
|
+
code,
|
|
57
|
+
message,
|
|
58
|
+
path,
|
|
59
|
+
requestID,
|
|
60
|
+
}: {
|
|
61
|
+
code?: string;
|
|
62
|
+
message?: string;
|
|
63
|
+
path: string;
|
|
64
|
+
requestID: string;
|
|
65
|
+
}) {
|
|
66
|
+
super();
|
|
67
|
+
this.message =
|
|
68
|
+
message ?? \`The requested path '\${path}' could not be found.\`;
|
|
69
|
+
this.requestID = requestID;
|
|
70
|
+
if (code) this.code = code;
|
|
71
|
+
}
|
|
72
|
+
}`,
|
|
73
|
+
skipIfExists: true,
|
|
74
|
+
integrateTarget: false,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
path: 'src/common/exceptions/conflict.exception.ts',
|
|
78
|
+
content: `export class ConflictException extends Error {
|
|
79
|
+
readonly status = 409;
|
|
80
|
+
readonly name = 'ConflictException';
|
|
81
|
+
readonly requestID: string;
|
|
82
|
+
|
|
83
|
+
constructor({
|
|
84
|
+
message,
|
|
85
|
+
requestID,
|
|
86
|
+
}: {
|
|
87
|
+
message?: string;
|
|
88
|
+
error?: string;
|
|
89
|
+
requestID: string;
|
|
90
|
+
}) {
|
|
91
|
+
super();
|
|
92
|
+
this.message = message ?? 'Conflict';
|
|
93
|
+
this.requestID = requestID;
|
|
94
|
+
}
|
|
95
|
+
}`,
|
|
96
|
+
skipIfExists: true,
|
|
97
|
+
integrateTarget: false,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
path: 'src/common/exceptions/unprocessable-entity.exception.ts',
|
|
101
|
+
content: `export interface UnprocessableEntityError {
|
|
102
|
+
code: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export class UnprocessableEntityException extends Error {
|
|
106
|
+
readonly status = 422;
|
|
107
|
+
readonly name = 'UnprocessableEntityException';
|
|
108
|
+
readonly requestID: string;
|
|
109
|
+
readonly code?: string;
|
|
110
|
+
|
|
111
|
+
constructor({
|
|
112
|
+
code,
|
|
113
|
+
errors,
|
|
114
|
+
message,
|
|
115
|
+
requestID,
|
|
116
|
+
}: {
|
|
117
|
+
code?: string;
|
|
118
|
+
errors?: UnprocessableEntityError[];
|
|
119
|
+
message?: string;
|
|
120
|
+
requestID: string;
|
|
121
|
+
}) {
|
|
122
|
+
super();
|
|
123
|
+
this.requestID = requestID;
|
|
124
|
+
this.message = message ?? 'Unprocessable entity';
|
|
125
|
+
if (code) this.code = code;
|
|
126
|
+
if (errors) {
|
|
127
|
+
const requirement =
|
|
128
|
+
errors.length === 1 ? 'requirement' : 'requirements';
|
|
129
|
+
this.message = \`The following \${requirement} must be met:\\n\`;
|
|
130
|
+
for (const { code: errCode } of errors) {
|
|
131
|
+
this.message = this.message.concat(\`\\t\${errCode}\\n\`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}`,
|
|
136
|
+
skipIfExists: true,
|
|
137
|
+
integrateTarget: false,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
path: 'src/common/exceptions/rate-limit-exceeded.exception.ts',
|
|
141
|
+
content: `export class RateLimitExceededException extends Error {
|
|
142
|
+
readonly status = 429;
|
|
143
|
+
readonly name = 'RateLimitExceededException';
|
|
144
|
+
readonly requestID: string;
|
|
145
|
+
readonly retryAfter?: number;
|
|
146
|
+
|
|
147
|
+
constructor(message: string, requestID: string, retryAfter?: number) {
|
|
148
|
+
super();
|
|
149
|
+
this.message = message ?? 'Too many requests';
|
|
150
|
+
this.requestID = requestID;
|
|
151
|
+
this.retryAfter = retryAfter;
|
|
152
|
+
}
|
|
153
|
+
}`,
|
|
154
|
+
skipIfExists: true,
|
|
155
|
+
integrateTarget: false,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
path: 'src/common/exceptions/generic-server.exception.ts',
|
|
159
|
+
content: `export class GenericServerException extends Error {
|
|
160
|
+
readonly status: number;
|
|
161
|
+
readonly name = 'GenericServerException';
|
|
162
|
+
readonly requestID: string;
|
|
163
|
+
|
|
164
|
+
constructor(status: number, message: string, requestID: string) {
|
|
165
|
+
super();
|
|
166
|
+
this.status = status;
|
|
167
|
+
this.message = message ?? 'Server error';
|
|
168
|
+
this.requestID = requestID;
|
|
169
|
+
}
|
|
170
|
+
}`,
|
|
171
|
+
skipIfExists: true,
|
|
172
|
+
integrateTarget: false,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
path: 'src/common/exceptions/no-api-key-provided.exception.ts',
|
|
176
|
+
content: `export class NoApiKeyProvidedException extends Error {
|
|
177
|
+
readonly name = 'NoApiKeyProvidedException';
|
|
178
|
+
|
|
179
|
+
constructor() {
|
|
180
|
+
super();
|
|
181
|
+
this.message =
|
|
182
|
+
'No API key provided. Pass it to the WorkOS constructor or set the WORKOS_API_KEY environment variable.';
|
|
183
|
+
}
|
|
184
|
+
}`,
|
|
185
|
+
skipIfExists: true,
|
|
186
|
+
integrateTarget: false,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
path: 'src/common/exceptions/index.ts',
|
|
190
|
+
content: `export { BadRequestException } from './bad-request.exception';
|
|
191
|
+
export { UnauthorizedException } from './unauthorized.exception';
|
|
192
|
+
export { NotFoundException } from './not-found.exception';
|
|
193
|
+
export { ConflictException } from './conflict.exception';
|
|
194
|
+
export {
|
|
195
|
+
UnprocessableEntityException,
|
|
196
|
+
type UnprocessableEntityError,
|
|
197
|
+
} from './unprocessable-entity.exception';
|
|
198
|
+
export { RateLimitExceededException } from './rate-limit-exceeded.exception';
|
|
199
|
+
export { GenericServerException } from './generic-server.exception';
|
|
200
|
+
export { NoApiKeyProvidedException } from './no-api-key-provided.exception';`,
|
|
201
|
+
skipIfExists: true,
|
|
202
|
+
integrateTarget: false,
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { Model, TypeRef, Enum, EmitterContext } from '@workos/oagen';
|
|
2
|
+
import { wireFieldName, fileName, serviceDirName, buildServiceNameMap, resolveServiceName } from './naming.js';
|
|
3
|
+
import { assignModelsToServices } from './utils.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate JSON fixture files for test data.
|
|
7
|
+
* Each model that appears as a response gets a fixture in wire format (snake_case).
|
|
8
|
+
*/
|
|
9
|
+
export function generateFixtures(
|
|
10
|
+
spec: {
|
|
11
|
+
models: Model[];
|
|
12
|
+
enums: Enum[];
|
|
13
|
+
services: any[];
|
|
14
|
+
},
|
|
15
|
+
ctx?: EmitterContext,
|
|
16
|
+
): { path: string; content: string }[] {
|
|
17
|
+
if (spec.models.length === 0) return [];
|
|
18
|
+
|
|
19
|
+
const modelToService = assignModelsToServices(spec.models, spec.services);
|
|
20
|
+
const serviceNameMap = ctx ? buildServiceNameMap(ctx.spec.services, ctx) : new Map<string, string>();
|
|
21
|
+
const resolveDir = (irService: string | undefined) =>
|
|
22
|
+
irService ? serviceDirName(serviceNameMap.get(irService) ?? irService) : 'common';
|
|
23
|
+
const modelMap = new Map(spec.models.map((m) => [m.name, m]));
|
|
24
|
+
const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
|
|
25
|
+
const files: { path: string; content: string }[] = [];
|
|
26
|
+
|
|
27
|
+
for (const model of spec.models) {
|
|
28
|
+
const service = modelToService.get(model.name);
|
|
29
|
+
const dirName = resolveDir(service);
|
|
30
|
+
const fixture = generateModelFixture(model, modelMap, enumMap);
|
|
31
|
+
|
|
32
|
+
files.push({
|
|
33
|
+
path: `src/${dirName}/fixtures/${fileName(model.name)}.fixture.json`,
|
|
34
|
+
content: JSON.stringify(fixture, null, 2),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Generate list fixtures for models that appear in paginated responses
|
|
39
|
+
for (const service of spec.services) {
|
|
40
|
+
const resolvedName = ctx ? resolveServiceName(service, ctx) : service.name;
|
|
41
|
+
const serviceDir = serviceDirName(resolvedName);
|
|
42
|
+
for (const op of service.operations) {
|
|
43
|
+
if (op.pagination) {
|
|
44
|
+
const itemModel = op.pagination.itemType.kind === 'model' ? modelMap.get(op.pagination.itemType.name) : null;
|
|
45
|
+
if (itemModel) {
|
|
46
|
+
const fixture = generateModelFixture(itemModel, modelMap, enumMap);
|
|
47
|
+
const listFixture = {
|
|
48
|
+
data: [fixture],
|
|
49
|
+
list_metadata: {
|
|
50
|
+
before: null,
|
|
51
|
+
after: null,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
files.push({
|
|
55
|
+
path: `src/${serviceDir}/fixtures/list-${fileName(itemModel.name)}.fixture.json`,
|
|
56
|
+
content: JSON.stringify(listFixture, null, 2),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function generateModelFixture(
|
|
67
|
+
model: Model,
|
|
68
|
+
modelMap: Map<string, Model>,
|
|
69
|
+
enumMap: Map<string, Enum>,
|
|
70
|
+
): Record<string, any> {
|
|
71
|
+
const fixture: Record<string, any> = {};
|
|
72
|
+
|
|
73
|
+
for (const field of model.fields) {
|
|
74
|
+
const wireName = wireFieldName(field.name);
|
|
75
|
+
fixture[wireName] = generateFieldValue(field.type, field.name, modelMap, enumMap);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return fixture;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function generateFieldValue(
|
|
82
|
+
ref: TypeRef,
|
|
83
|
+
fieldName: string,
|
|
84
|
+
modelMap: Map<string, Model>,
|
|
85
|
+
enumMap: Map<string, Enum>,
|
|
86
|
+
): any {
|
|
87
|
+
switch (ref.kind) {
|
|
88
|
+
case 'primitive':
|
|
89
|
+
return generatePrimitiveValue(ref.type, ref.format, fieldName);
|
|
90
|
+
case 'literal':
|
|
91
|
+
return ref.value;
|
|
92
|
+
case 'enum': {
|
|
93
|
+
const e = enumMap.get(ref.name);
|
|
94
|
+
return e?.values[0]?.value ?? 'unknown';
|
|
95
|
+
}
|
|
96
|
+
case 'model': {
|
|
97
|
+
const nested = modelMap.get(ref.name);
|
|
98
|
+
if (nested) return generateModelFixture(nested, modelMap, enumMap);
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
case 'array': {
|
|
102
|
+
const item = generateFieldValue(ref.items, fieldName, modelMap, enumMap);
|
|
103
|
+
return [item];
|
|
104
|
+
}
|
|
105
|
+
case 'nullable':
|
|
106
|
+
return generateFieldValue(ref.inner, fieldName, modelMap, enumMap);
|
|
107
|
+
case 'union':
|
|
108
|
+
if (ref.variants.length > 0) {
|
|
109
|
+
return generateFieldValue(ref.variants[0], fieldName, modelMap, enumMap);
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
case 'map':
|
|
113
|
+
return { key: generateFieldValue(ref.valueType, 'value', modelMap, enumMap) };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function generatePrimitiveValue(type: string, format: string | undefined, name: string): any {
|
|
118
|
+
switch (type) {
|
|
119
|
+
case 'string':
|
|
120
|
+
if (format === 'date-time') return '2023-01-01T00:00:00.000Z';
|
|
121
|
+
if (format === 'date') return '2023-01-01';
|
|
122
|
+
if (format === 'uuid') return '00000000-0000-0000-0000-000000000000';
|
|
123
|
+
if (name.includes('id')) return `${name}_01234`;
|
|
124
|
+
if (name.includes('email')) return 'test@example.com';
|
|
125
|
+
if (name.includes('url') || name.includes('uri')) return 'https://example.com';
|
|
126
|
+
if (name.includes('name')) return 'Test';
|
|
127
|
+
return `test_${name}`;
|
|
128
|
+
case 'integer':
|
|
129
|
+
return 1;
|
|
130
|
+
case 'number':
|
|
131
|
+
return 1.0;
|
|
132
|
+
case 'boolean':
|
|
133
|
+
return true;
|
|
134
|
+
case 'unknown':
|
|
135
|
+
return {};
|
|
136
|
+
default:
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Emitter, EmitterContext, GeneratedFile, ApiSpec, Model, Enum, Service } from '@workos/oagen';
|
|
2
|
+
|
|
3
|
+
import { generateModels } from './models.js';
|
|
4
|
+
import { generateEnums } from './enums.js';
|
|
5
|
+
import { generateSerializers } from './serializers.js';
|
|
6
|
+
import { generateResources } from './resources.js';
|
|
7
|
+
import { generateClient } from './client.js';
|
|
8
|
+
import { generateErrors } from './errors.js';
|
|
9
|
+
import { generateConfig } from './config.js';
|
|
10
|
+
import { generateCommon } from './common.js';
|
|
11
|
+
import { generateTests } from './tests.js';
|
|
12
|
+
import { generateManifest } from './manifest.js';
|
|
13
|
+
|
|
14
|
+
export const nodeEmitter: Emitter = {
|
|
15
|
+
language: 'node',
|
|
16
|
+
|
|
17
|
+
generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
|
|
18
|
+
return [...generateModels(models, ctx), ...generateSerializers(models, ctx)];
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
22
|
+
return generateEnums(enums, ctx);
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
|
26
|
+
return generateResources(services, ctx);
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
30
|
+
return generateClient(spec, ctx);
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
generateErrors(_ctx: EmitterContext): GeneratedFile[] {
|
|
34
|
+
return generateErrors();
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
generateConfig(_ctx: EmitterContext): GeneratedFile[] {
|
|
38
|
+
return [...generateConfig(), ...generateCommon()];
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
generateTypeSignatures(_spec: ApiSpec, _ctx: EmitterContext): GeneratedFile[] {
|
|
42
|
+
// TypeScript uses inline types — no separate type signature files needed
|
|
43
|
+
return [];
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
47
|
+
return generateTests(spec, ctx);
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
generateManifest(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
51
|
+
return generateManifest(spec, ctx);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
fileHeader(): string {
|
|
55
|
+
return '// This file is auto-generated by oagen. Do not edit.';
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ApiSpec, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
|
+
import { resolveMethodName, servicePropertyName, resolveServiceName } from './naming.js';
|
|
3
|
+
|
|
4
|
+
export function generateManifest(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
5
|
+
const manifest: Record<string, { sdkMethod: string; service: string }> = {};
|
|
6
|
+
|
|
7
|
+
for (const service of spec.services) {
|
|
8
|
+
const propName = servicePropertyName(resolveServiceName(service, ctx));
|
|
9
|
+
for (const op of service.operations) {
|
|
10
|
+
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
11
|
+
const method = resolveMethodName(op, service, ctx);
|
|
12
|
+
manifest[httpKey] = { sdkMethod: method, service: propName };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
path: 'smoke-manifest.json',
|
|
19
|
+
content: JSON.stringify(manifest, null, 2),
|
|
20
|
+
integrateTarget: false,
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
}
|