@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,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DotNet (C#) extractor — delegates to the canonical implementation in @workos/oagen.
|
|
3
|
+
*
|
|
4
|
+
* Re-exported here so the emitter project can:
|
|
5
|
+
* 1. Register it in oagen.config.ts alongside the emitter
|
|
6
|
+
* 2. Customize hints if the generated SDK deviates from core defaults
|
|
7
|
+
*/
|
|
8
|
+
export { dotnetExtractor } from '@workos/oagen/compat';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Elixir extractor — delegates to the canonical implementation in @workos/oagen.
|
|
3
|
+
*
|
|
4
|
+
* Re-exported here so the emitter project can:
|
|
5
|
+
* 1. Register it in oagen.config.ts alongside the emitter
|
|
6
|
+
* 2. Customize hints if the generated SDK deviates from core defaults
|
|
7
|
+
*/
|
|
8
|
+
export { elixirExtractor } from '@workos/oagen/compat';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Go extractor — delegates to the canonical implementation in @workos/oagen.
|
|
3
|
+
*
|
|
4
|
+
* Re-exported here so the emitter project can:
|
|
5
|
+
* 1. Register it in oagen.config.ts alongside the emitter
|
|
6
|
+
* 2. Customize hints if the generated SDK deviates from core defaults
|
|
7
|
+
*/
|
|
8
|
+
export { goExtractor } from '@workos/oagen/compat';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kotlin extractor — delegates to the canonical implementation in @workos/oagen.
|
|
3
|
+
*
|
|
4
|
+
* Re-exported here so the emitter project can:
|
|
5
|
+
* 1. Register it in oagen.config.ts alongside the emitter
|
|
6
|
+
* 2. Customize hints if the generated SDK deviates from core defaults
|
|
7
|
+
*/
|
|
8
|
+
export { kotlinExtractor } from '@workos/oagen/compat';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node/TypeScript extractor — delegates to the canonical implementation in @workos/oagen.
|
|
3
|
+
*
|
|
4
|
+
* Re-exported here so the emitter project can:
|
|
5
|
+
* 1. Register it in oagen.config.ts alongside the emitter
|
|
6
|
+
* 2. Customize hints if the generated SDK deviates from core defaults
|
|
7
|
+
*/
|
|
8
|
+
export { nodeExtractor } from '@workos/oagen/compat';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHP extractor — delegates to the canonical implementation in @workos/oagen.
|
|
3
|
+
*
|
|
4
|
+
* Re-exported here so the emitter project can:
|
|
5
|
+
* 1. Register it in oagen.config.ts alongside the emitter
|
|
6
|
+
* 2. Customize hints if the generated SDK deviates from core defaults
|
|
7
|
+
*/
|
|
8
|
+
export { phpExtractor } from '@workos/oagen/compat';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python extractor — delegates to the canonical implementation in @workos/oagen.
|
|
3
|
+
*
|
|
4
|
+
* Re-exported here so the emitter project can:
|
|
5
|
+
* 1. Register it in oagen.config.ts alongside the emitter
|
|
6
|
+
* 2. Customize hints if the generated SDK deviates from core defaults
|
|
7
|
+
*/
|
|
8
|
+
export { pythonExtractor } from '@workos/oagen/compat';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruby extractor — delegates to the canonical implementation in @workos/oagen.
|
|
3
|
+
*
|
|
4
|
+
* Re-exported here so the emitter project can:
|
|
5
|
+
* 1. Register it in oagen.config.ts alongside the emitter
|
|
6
|
+
* 2. Customize hints if the generated SDK deviates from core defaults
|
|
7
|
+
*/
|
|
8
|
+
export { rubyExtractor } from '@workos/oagen/compat';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rust extractor — delegates to the canonical implementation in @workos/oagen.
|
|
3
|
+
*
|
|
4
|
+
* Re-exported here so the emitter project can:
|
|
5
|
+
* 1. Register it in oagen.config.ts alongside the emitter
|
|
6
|
+
* 2. Customize hints if the generated SDK deviates from core defaults
|
|
7
|
+
*/
|
|
8
|
+
export { rustExtractor } from '@workos/oagen/compat';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { nodeEmitter } from './node/index.js';
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import type { ApiSpec, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
|
|
2
|
+
import {
|
|
3
|
+
fileName,
|
|
4
|
+
serviceDirName,
|
|
5
|
+
servicePropertyName,
|
|
6
|
+
resolveInterfaceName,
|
|
7
|
+
resolveServiceName,
|
|
8
|
+
buildServiceNameMap,
|
|
9
|
+
wireInterfaceName,
|
|
10
|
+
} from './naming.js';
|
|
11
|
+
import { assignModelsToServices } from './utils.js';
|
|
12
|
+
|
|
13
|
+
export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
14
|
+
const files: GeneratedFile[] = [];
|
|
15
|
+
|
|
16
|
+
files.push(generateWorkOSClient(spec, ctx));
|
|
17
|
+
files.push(generateBarrel(spec, ctx));
|
|
18
|
+
files.push(generatePackageJson(ctx));
|
|
19
|
+
files.push(generateTsConfig());
|
|
20
|
+
|
|
21
|
+
return files;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
|
|
25
|
+
const lines: string[] = [];
|
|
26
|
+
|
|
27
|
+
// Service imports
|
|
28
|
+
for (const service of spec.services) {
|
|
29
|
+
const resolvedName = resolveServiceName(service, ctx);
|
|
30
|
+
const serviceDir = serviceDirName(resolvedName);
|
|
31
|
+
lines.push(`import { ${resolvedName} } from './${serviceDir}/${fileName(resolvedName)}';`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
lines.push("import type { WorkOSOptions } from './common/interfaces/workos-options.interface';");
|
|
35
|
+
lines.push("import type { PostOptions } from './common/interfaces/post-options.interface';");
|
|
36
|
+
lines.push("import type { GetOptions } from './common/interfaces/get-options.interface';");
|
|
37
|
+
lines.push("import { NoApiKeyProvidedException } from './common/exceptions/no-api-key-provided.exception';");
|
|
38
|
+
lines.push("import { UnauthorizedException } from './common/exceptions/unauthorized.exception';");
|
|
39
|
+
lines.push("import { NotFoundException } from './common/exceptions/not-found.exception';");
|
|
40
|
+
lines.push("import { ConflictException } from './common/exceptions/conflict.exception';");
|
|
41
|
+
lines.push("import { UnprocessableEntityException } from './common/exceptions/unprocessable-entity.exception';");
|
|
42
|
+
lines.push("import { RateLimitExceededException } from './common/exceptions/rate-limit-exceeded.exception';");
|
|
43
|
+
lines.push("import { GenericServerException } from './common/exceptions/generic-server.exception';");
|
|
44
|
+
lines.push("import { BadRequestException } from './common/exceptions/bad-request.exception';");
|
|
45
|
+
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push('export class WorkOS {');
|
|
48
|
+
lines.push(' readonly baseURL: string;');
|
|
49
|
+
lines.push(' readonly key: string;');
|
|
50
|
+
lines.push(' private readonly options: WorkOSOptions;');
|
|
51
|
+
lines.push('');
|
|
52
|
+
|
|
53
|
+
// Resource accessors
|
|
54
|
+
for (const service of spec.services) {
|
|
55
|
+
const resolvedName = resolveServiceName(service, ctx);
|
|
56
|
+
const propName = servicePropertyName(resolvedName);
|
|
57
|
+
lines.push(` readonly ${propName} = new ${resolvedName}(this);`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
lines.push('');
|
|
61
|
+
lines.push(' constructor(keyOrOptions?: string | WorkOSOptions, maybeOptions?: WorkOSOptions) {');
|
|
62
|
+
lines.push(" if (typeof keyOrOptions === 'object') {");
|
|
63
|
+
lines.push(" this.key = keyOrOptions.apiKey ?? '';");
|
|
64
|
+
lines.push(' this.options = keyOrOptions;');
|
|
65
|
+
lines.push(' } else {');
|
|
66
|
+
lines.push(" this.key = keyOrOptions ?? '';");
|
|
67
|
+
lines.push(' this.options = maybeOptions ?? {};');
|
|
68
|
+
lines.push(' }');
|
|
69
|
+
lines.push('');
|
|
70
|
+
lines.push(' if (!this.key) {');
|
|
71
|
+
lines.push(" const envKey = typeof process !== 'undefined' ? process.env?.WORKOS_API_KEY : undefined;");
|
|
72
|
+
lines.push(' if (envKey) this.key = envKey;');
|
|
73
|
+
lines.push(' }');
|
|
74
|
+
lines.push('');
|
|
75
|
+
lines.push(" const protocol = this.options.https === false ? 'http' : 'https';");
|
|
76
|
+
lines.push(" const hostname = this.options.apiHostname ?? 'api.workos.com';");
|
|
77
|
+
lines.push(" const port = this.options.port ? `:${this.options.port}` : '';");
|
|
78
|
+
lines.push(' this.baseURL = `${protocol}://${hostname}${port}`;');
|
|
79
|
+
lines.push(' }');
|
|
80
|
+
|
|
81
|
+
// HTTP methods
|
|
82
|
+
lines.push('');
|
|
83
|
+
lines.push(' async get<Result = any>(path: string, options: GetOptions = {}): Promise<{ data: Result }> {');
|
|
84
|
+
lines.push(' this.ensureApiKey(options);');
|
|
85
|
+
lines.push(' const url = this.buildUrl(path, options.query);');
|
|
86
|
+
lines.push(' const response = await fetch(url, {');
|
|
87
|
+
lines.push(" method: 'GET',");
|
|
88
|
+
lines.push(' headers: this.buildHeaders(options),');
|
|
89
|
+
lines.push(' });');
|
|
90
|
+
lines.push(' await this.handleHttpError(response, path);');
|
|
91
|
+
lines.push(' const data = await response.json() as Result;');
|
|
92
|
+
lines.push(' return { data };');
|
|
93
|
+
lines.push(' }');
|
|
94
|
+
|
|
95
|
+
lines.push('');
|
|
96
|
+
lines.push(
|
|
97
|
+
' async post<Result = any, Entity = any>(path: string, entity: Entity, options: PostOptions = {}): Promise<{ data: Result }> {',
|
|
98
|
+
);
|
|
99
|
+
lines.push(' this.ensureApiKey(options);');
|
|
100
|
+
lines.push(' const url = this.buildUrl(path, options.query);');
|
|
101
|
+
lines.push(' const response = await fetch(url, {');
|
|
102
|
+
lines.push(" method: 'POST',");
|
|
103
|
+
lines.push(' headers: this.buildHeaders(options),');
|
|
104
|
+
lines.push(' body: JSON.stringify(entity),');
|
|
105
|
+
lines.push(' });');
|
|
106
|
+
lines.push(' await this.handleHttpError(response, path);');
|
|
107
|
+
lines.push(' const data = await response.json() as Result;');
|
|
108
|
+
lines.push(' return { data };');
|
|
109
|
+
lines.push(' }');
|
|
110
|
+
|
|
111
|
+
lines.push('');
|
|
112
|
+
lines.push(
|
|
113
|
+
' async put<Result = any, Entity = any>(path: string, entity: Entity, options: PostOptions = {}): Promise<{ data: Result }> {',
|
|
114
|
+
);
|
|
115
|
+
lines.push(' this.ensureApiKey(options);');
|
|
116
|
+
lines.push(' const url = this.buildUrl(path, options.query);');
|
|
117
|
+
lines.push(' const response = await fetch(url, {');
|
|
118
|
+
lines.push(" method: 'PUT',");
|
|
119
|
+
lines.push(' headers: this.buildHeaders(options),');
|
|
120
|
+
lines.push(' body: JSON.stringify(entity),');
|
|
121
|
+
lines.push(' });');
|
|
122
|
+
lines.push(' await this.handleHttpError(response, path);');
|
|
123
|
+
lines.push(' const data = await response.json() as Result;');
|
|
124
|
+
lines.push(' return { data };');
|
|
125
|
+
lines.push(' }');
|
|
126
|
+
|
|
127
|
+
lines.push('');
|
|
128
|
+
lines.push(
|
|
129
|
+
' async patch<Result = any, Entity = any>(path: string, entity: Entity, options: PostOptions = {}): Promise<{ data: Result }> {',
|
|
130
|
+
);
|
|
131
|
+
lines.push(' this.ensureApiKey(options);');
|
|
132
|
+
lines.push(' const url = this.buildUrl(path, options.query);');
|
|
133
|
+
lines.push(' const response = await fetch(url, {');
|
|
134
|
+
lines.push(" method: 'PATCH',");
|
|
135
|
+
lines.push(' headers: this.buildHeaders(options),');
|
|
136
|
+
lines.push(' body: JSON.stringify(entity),');
|
|
137
|
+
lines.push(' });');
|
|
138
|
+
lines.push(' await this.handleHttpError(response, path);');
|
|
139
|
+
lines.push(' const data = await response.json() as Result;');
|
|
140
|
+
lines.push(' return { data };');
|
|
141
|
+
lines.push(' }');
|
|
142
|
+
|
|
143
|
+
lines.push('');
|
|
144
|
+
lines.push(' async delete(path: string, options: GetOptions = {}): Promise<void> {');
|
|
145
|
+
lines.push(' this.ensureApiKey(options);');
|
|
146
|
+
lines.push(' const url = this.buildUrl(path);');
|
|
147
|
+
lines.push(' const response = await fetch(url, {');
|
|
148
|
+
lines.push(" method: 'DELETE',");
|
|
149
|
+
lines.push(' headers: this.buildHeaders(options),');
|
|
150
|
+
lines.push(' });');
|
|
151
|
+
lines.push(' await this.handleHttpError(response, path);');
|
|
152
|
+
lines.push(' }');
|
|
153
|
+
|
|
154
|
+
// Private helpers
|
|
155
|
+
lines.push('');
|
|
156
|
+
lines.push(' private buildUrl(path: string, query?: Record<string, any>): string {');
|
|
157
|
+
lines.push(' const url = new URL(path, this.baseURL);');
|
|
158
|
+
lines.push(' if (query) {');
|
|
159
|
+
lines.push(' for (const [key, value] of Object.entries(query)) {');
|
|
160
|
+
lines.push(" if (value !== null && value !== undefined && value !== '') {");
|
|
161
|
+
lines.push(' url.searchParams.set(key, String(value));');
|
|
162
|
+
lines.push(' }');
|
|
163
|
+
lines.push(' }');
|
|
164
|
+
lines.push(' }');
|
|
165
|
+
lines.push(' return url.toString();');
|
|
166
|
+
lines.push(' }');
|
|
167
|
+
|
|
168
|
+
lines.push('');
|
|
169
|
+
lines.push(' private buildHeaders(options: any = {}): Record<string, string> {');
|
|
170
|
+
lines.push(' const headers: Record<string, string> = {');
|
|
171
|
+
lines.push(" 'Content-Type': 'application/json',");
|
|
172
|
+
lines.push(` Authorization: \`Bearer \${this.key}\`,`);
|
|
173
|
+
lines.push(' };');
|
|
174
|
+
lines.push(" if (options.idempotencyKey) headers['Idempotency-Key'] = options.idempotencyKey;");
|
|
175
|
+
lines.push(" if (options.warrantToken) headers['Warrant-Token'] = options.warrantToken;");
|
|
176
|
+
lines.push(' return headers;');
|
|
177
|
+
lines.push(' }');
|
|
178
|
+
|
|
179
|
+
lines.push('');
|
|
180
|
+
lines.push(' private ensureApiKey(options: any = {}): void {');
|
|
181
|
+
lines.push(' if (!this.key && !options.skipApiKeyCheck) {');
|
|
182
|
+
lines.push(' throw new NoApiKeyProvidedException();');
|
|
183
|
+
lines.push(' }');
|
|
184
|
+
lines.push(' }');
|
|
185
|
+
|
|
186
|
+
lines.push('');
|
|
187
|
+
lines.push(' private async handleHttpError(response: Response, path: string): Promise<void> {');
|
|
188
|
+
lines.push(' if (response.ok) return;');
|
|
189
|
+
lines.push('');
|
|
190
|
+
lines.push(" const requestID = response.headers.get('x-request-id') ?? '';");
|
|
191
|
+
lines.push(' let data: any = {};');
|
|
192
|
+
lines.push(' try { data = await response.json(); } catch {}');
|
|
193
|
+
lines.push(' const { message, code, errors } = data;');
|
|
194
|
+
lines.push('');
|
|
195
|
+
lines.push(' switch (response.status) {');
|
|
196
|
+
lines.push(' case 400: throw new BadRequestException({ code, message, requestID });');
|
|
197
|
+
lines.push(' case 401: throw new UnauthorizedException(requestID);');
|
|
198
|
+
lines.push(' case 404: throw new NotFoundException({ code, message, path, requestID });');
|
|
199
|
+
lines.push(' case 409: throw new ConflictException({ message, requestID });');
|
|
200
|
+
lines.push(' case 422: throw new UnprocessableEntityException({ code, errors, message, requestID });');
|
|
201
|
+
lines.push(' case 429: {');
|
|
202
|
+
lines.push(" const retryAfter = Number(response.headers.get('retry-after')) || undefined;");
|
|
203
|
+
lines.push(" throw new RateLimitExceededException(message ?? 'Too many requests', requestID, retryAfter);");
|
|
204
|
+
lines.push(' }');
|
|
205
|
+
lines.push(" default: throw new GenericServerException(response.status, message ?? 'Server error', requestID);");
|
|
206
|
+
lines.push(' }');
|
|
207
|
+
lines.push(' }');
|
|
208
|
+
|
|
209
|
+
lines.push('}');
|
|
210
|
+
|
|
211
|
+
return { path: 'src/workos.ts', content: lines.join('\n'), skipIfExists: true, integrateTarget: false };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Names exported from common utilities that must not be re-exported from model interfaces
|
|
215
|
+
const RESERVED_BARREL_NAMES = new Set(['List', 'ListResponse', 'ListMetadata', 'AutoPaginatable', 'PaginationOptions']);
|
|
216
|
+
|
|
217
|
+
function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
|
|
218
|
+
const lines: string[] = [];
|
|
219
|
+
const modelToService = assignModelsToServices(spec.models, spec.services);
|
|
220
|
+
const serviceNameMap = buildServiceNameMap(spec.services, ctx);
|
|
221
|
+
const resolveDir = (irService: string | undefined) =>
|
|
222
|
+
irService ? serviceDirName(serviceNameMap.get(irService) ?? irService) : 'common';
|
|
223
|
+
|
|
224
|
+
// Common exports
|
|
225
|
+
lines.push("export * from './common/exceptions';");
|
|
226
|
+
lines.push("export { AutoPaginatable } from './common/utils/pagination';");
|
|
227
|
+
lines.push("export type { List, ListMetadata, ListResponse } from './common/utils/pagination';");
|
|
228
|
+
lines.push("export type { PaginationOptions } from './common/interfaces/pagination-options.interface';");
|
|
229
|
+
lines.push("export type { WorkOSOptions } from './common/interfaces/workos-options.interface';");
|
|
230
|
+
lines.push("export type { PostOptions } from './common/interfaces/post-options.interface';");
|
|
231
|
+
lines.push("export type { GetOptions } from './common/interfaces/get-options.interface';");
|
|
232
|
+
lines.push('');
|
|
233
|
+
|
|
234
|
+
// Per-service exports: interfaces + resource class
|
|
235
|
+
for (const service of spec.services) {
|
|
236
|
+
const resolvedName = resolveServiceName(service, ctx);
|
|
237
|
+
const serviceDir = serviceDirName(resolvedName);
|
|
238
|
+
|
|
239
|
+
// Collect models that belong to this service, skipping reserved names
|
|
240
|
+
const serviceModels = spec.models.filter((m) => modelToService.get(m.name) === service.name);
|
|
241
|
+
for (const model of serviceModels) {
|
|
242
|
+
const name = resolveInterfaceName(model.name, ctx);
|
|
243
|
+
const wireName = wireInterfaceName(name);
|
|
244
|
+
if (RESERVED_BARREL_NAMES.has(name) || RESERVED_BARREL_NAMES.has(wireName)) continue;
|
|
245
|
+
lines.push(
|
|
246
|
+
`export type { ${name}, ${wireName} } from './${serviceDir}/interfaces/${fileName(model.name)}.interface';`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Resource class
|
|
251
|
+
lines.push(`export { ${resolvedName} } from './${serviceDir}/${fileName(resolvedName)}';`);
|
|
252
|
+
lines.push('');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Unassigned models (common), skipping reserved names
|
|
256
|
+
const unassignedModels = spec.models.filter((m) => !modelToService.has(m.name));
|
|
257
|
+
for (const model of unassignedModels) {
|
|
258
|
+
const name = resolveInterfaceName(model.name, ctx);
|
|
259
|
+
const wireName = wireInterfaceName(name);
|
|
260
|
+
if (RESERVED_BARREL_NAMES.has(name) || RESERVED_BARREL_NAMES.has(wireName)) continue;
|
|
261
|
+
lines.push(`export type { ${name}, ${wireName} } from './common/interfaces/${fileName(model.name)}.interface';`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Enum exports
|
|
265
|
+
for (const enumDef of spec.enums) {
|
|
266
|
+
// Find which service directory the enum landed in
|
|
267
|
+
const enumService = findEnumService(enumDef.name, spec.services);
|
|
268
|
+
const dir = resolveDir(enumService);
|
|
269
|
+
lines.push(`export type { ${enumDef.name} } from './${dir}/interfaces/${fileName(enumDef.name)}.interface';`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
lines.push('');
|
|
273
|
+
lines.push("export { WorkOS } from './workos';");
|
|
274
|
+
|
|
275
|
+
return { path: 'src/index.ts', content: lines.join('\n'), skipIfExists: true };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function findEnumService(enumName: string, services: Service[]): string | undefined {
|
|
279
|
+
for (const service of services) {
|
|
280
|
+
for (const op of service.operations) {
|
|
281
|
+
const refs: string[] = [];
|
|
282
|
+
const collect = (ref: any) => {
|
|
283
|
+
if (ref?.kind === 'enum' && ref.name === enumName) refs.push(ref.name);
|
|
284
|
+
if (ref?.items) collect(ref.items);
|
|
285
|
+
if (ref?.inner) collect(ref.inner);
|
|
286
|
+
if (ref?.variants) ref.variants.forEach(collect);
|
|
287
|
+
if (ref?.valueType) collect(ref.valueType);
|
|
288
|
+
};
|
|
289
|
+
if (op.requestBody) collect(op.requestBody);
|
|
290
|
+
collect(op.response);
|
|
291
|
+
for (const p of [...op.pathParams, ...op.queryParams]) {
|
|
292
|
+
collect(p.type);
|
|
293
|
+
}
|
|
294
|
+
if (refs.length > 0) return service.name;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function generatePackageJson(ctx: EmitterContext): GeneratedFile {
|
|
301
|
+
const pkg = {
|
|
302
|
+
name: `@${ctx.namespace}/sdk`,
|
|
303
|
+
version: '0.0.0',
|
|
304
|
+
type: 'module',
|
|
305
|
+
main: 'src/index.ts',
|
|
306
|
+
types: 'src/index.ts',
|
|
307
|
+
exports: {
|
|
308
|
+
'.': './src/index.ts',
|
|
309
|
+
},
|
|
310
|
+
scripts: {
|
|
311
|
+
test: 'jest',
|
|
312
|
+
build: 'tsc',
|
|
313
|
+
},
|
|
314
|
+
devDependencies: {
|
|
315
|
+
typescript: '^5.0.0',
|
|
316
|
+
jest: '^29.0.0',
|
|
317
|
+
'jest-fetch-mock': '^3.0.0',
|
|
318
|
+
'@types/jest': '^29.0.0',
|
|
319
|
+
'ts-jest': '^29.0.0',
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
path: 'package.json',
|
|
325
|
+
content: JSON.stringify(pkg, null, 2),
|
|
326
|
+
skipIfExists: true,
|
|
327
|
+
integrateTarget: false,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function generateTsConfig(): GeneratedFile {
|
|
332
|
+
const config = {
|
|
333
|
+
compilerOptions: {
|
|
334
|
+
target: 'ES2020',
|
|
335
|
+
module: 'CommonJS',
|
|
336
|
+
lib: ['ES2020'],
|
|
337
|
+
declaration: true,
|
|
338
|
+
strict: true,
|
|
339
|
+
esModuleInterop: true,
|
|
340
|
+
skipLibCheck: true,
|
|
341
|
+
forceConsistentCasingInFileNames: true,
|
|
342
|
+
resolveJsonModule: true,
|
|
343
|
+
outDir: './lib',
|
|
344
|
+
rootDir: './src',
|
|
345
|
+
},
|
|
346
|
+
include: ['src/**/*'],
|
|
347
|
+
exclude: ['node_modules', 'lib', '**/*.spec.ts'],
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
path: 'tsconfig.json',
|
|
352
|
+
content: JSON.stringify(config, null, 2),
|
|
353
|
+
skipIfExists: true,
|
|
354
|
+
integrateTarget: false,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { GeneratedFile } from '@workos/oagen';
|
|
2
|
+
|
|
3
|
+
export function generateCommon(): GeneratedFile[] {
|
|
4
|
+
return [
|
|
5
|
+
{
|
|
6
|
+
path: 'src/common/utils/pagination.ts',
|
|
7
|
+
content: paginationContent(),
|
|
8
|
+
skipIfExists: true,
|
|
9
|
+
integrateTarget: false,
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
path: 'src/common/utils/fetch-and-deserialize.ts',
|
|
13
|
+
content: fetchAndDeserializeContent(),
|
|
14
|
+
skipIfExists: true,
|
|
15
|
+
integrateTarget: false,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
path: 'src/common/serializers/list.serializer.ts',
|
|
19
|
+
content: listSerializerContent(),
|
|
20
|
+
skipIfExists: true,
|
|
21
|
+
integrateTarget: false,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
path: 'src/common/utils/test-utils.ts',
|
|
25
|
+
content: testUtilsContent(),
|
|
26
|
+
skipIfExists: true,
|
|
27
|
+
integrateTarget: false,
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function paginationContent(): string {
|
|
33
|
+
return `import type { PaginationOptions } from '../interfaces/pagination-options.interface';
|
|
34
|
+
|
|
35
|
+
export interface ListMetadata {
|
|
36
|
+
before: string | null;
|
|
37
|
+
after: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface List<T> {
|
|
41
|
+
object: 'list';
|
|
42
|
+
data: T[];
|
|
43
|
+
listMetadata: ListMetadata;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ListResponse<T> {
|
|
47
|
+
object: 'list';
|
|
48
|
+
data: T[];
|
|
49
|
+
list_metadata: {
|
|
50
|
+
before: string | null;
|
|
51
|
+
after: string | null;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class AutoPaginatable<
|
|
56
|
+
ResourceType,
|
|
57
|
+
ParametersType extends PaginationOptions = PaginationOptions,
|
|
58
|
+
> {
|
|
59
|
+
readonly object = 'list' as const;
|
|
60
|
+
readonly options: ParametersType;
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
protected list: List<ResourceType>,
|
|
64
|
+
private apiCall: (params: PaginationOptions) => Promise<List<ResourceType>>,
|
|
65
|
+
options?: ParametersType,
|
|
66
|
+
) {
|
|
67
|
+
this.options = options ?? ({} as ParametersType);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get data(): ResourceType[] {
|
|
71
|
+
return this.list.data;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get listMetadata() {
|
|
75
|
+
return this.list.listMetadata;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async *generatePages(
|
|
79
|
+
params: PaginationOptions,
|
|
80
|
+
): AsyncGenerator<ResourceType[]> {
|
|
81
|
+
const result = await this.apiCall({
|
|
82
|
+
...this.options,
|
|
83
|
+
limit: 100,
|
|
84
|
+
after: params.after,
|
|
85
|
+
});
|
|
86
|
+
yield result.data;
|
|
87
|
+
if (result.listMetadata.after) {
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
89
|
+
yield* this.generatePages({ after: result.listMetadata.after });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async autoPagination(): Promise<ResourceType[]> {
|
|
94
|
+
if (this.options.limit) {
|
|
95
|
+
return this.data;
|
|
96
|
+
}
|
|
97
|
+
const results: ResourceType[] = [];
|
|
98
|
+
for await (const page of this.generatePages({
|
|
99
|
+
after: this.options.after,
|
|
100
|
+
})) {
|
|
101
|
+
results.push(...page);
|
|
102
|
+
}
|
|
103
|
+
return results;
|
|
104
|
+
}
|
|
105
|
+
}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function fetchAndDeserializeContent(): string {
|
|
109
|
+
return `import type { WorkOS } from '../../workos';
|
|
110
|
+
import type { PaginationOptions } from '../interfaces/pagination-options.interface';
|
|
111
|
+
import type { List, ListResponse } from './pagination';
|
|
112
|
+
|
|
113
|
+
function setDefaultOptions(
|
|
114
|
+
options?: PaginationOptions,
|
|
115
|
+
): Record<string, any> {
|
|
116
|
+
return {
|
|
117
|
+
order: 'desc',
|
|
118
|
+
...options,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function deserializeList<T, U>(
|
|
123
|
+
data: ListResponse<T>,
|
|
124
|
+
deserializeFn: (item: T) => U,
|
|
125
|
+
): List<U> {
|
|
126
|
+
return {
|
|
127
|
+
data: data.data.map(deserializeFn),
|
|
128
|
+
listMetadata: {
|
|
129
|
+
before: data.list_metadata.before,
|
|
130
|
+
after: data.list_metadata.after,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const fetchAndDeserialize = async <T, U>(
|
|
136
|
+
workos: WorkOS,
|
|
137
|
+
endpoint: string,
|
|
138
|
+
deserializeFn: (data: T) => U,
|
|
139
|
+
options?: PaginationOptions,
|
|
140
|
+
): Promise<List<U>> => {
|
|
141
|
+
const { data } = await workos.get<ListResponse<T>>(endpoint, {
|
|
142
|
+
query: setDefaultOptions(options),
|
|
143
|
+
});
|
|
144
|
+
return deserializeList(data, deserializeFn);
|
|
145
|
+
};`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function listSerializerContent(): string {
|
|
149
|
+
return `import type { ListMetadata, ListResponse } from '../utils/pagination';
|
|
150
|
+
|
|
151
|
+
export function deserializeListMetadata(
|
|
152
|
+
metadata: ListResponse<any>['list_metadata'],
|
|
153
|
+
): ListMetadata {
|
|
154
|
+
return {
|
|
155
|
+
before: metadata.before,
|
|
156
|
+
after: metadata.after,
|
|
157
|
+
};
|
|
158
|
+
}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function testUtilsContent(): string {
|
|
162
|
+
return `import fetch from 'jest-fetch-mock';
|
|
163
|
+
|
|
164
|
+
interface MockParams {
|
|
165
|
+
status?: number;
|
|
166
|
+
headers?: Record<string, string>;
|
|
167
|
+
[key: string]: any;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function fetchOnce(
|
|
171
|
+
response: any = {},
|
|
172
|
+
{ status = 200, headers, ...rest }: MockParams = {},
|
|
173
|
+
) {
|
|
174
|
+
return fetch.once(JSON.stringify(response), {
|
|
175
|
+
status,
|
|
176
|
+
headers: {
|
|
177
|
+
'content-type': 'application/json;charset=UTF-8',
|
|
178
|
+
...headers,
|
|
179
|
+
},
|
|
180
|
+
...rest,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function fetchURL(): string {
|
|
185
|
+
return String(fetch.mock.calls[0][0]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function fetchSearchParams(): Record<string, string> {
|
|
189
|
+
return Object.fromEntries(new URL(fetchURL()).searchParams);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function fetchHeaders(): Record<string, string> {
|
|
193
|
+
const headers = fetch.mock.calls[0][1]?.headers ?? {};
|
|
194
|
+
return headers as Record<string, string>;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function fetchBody({ raw = false } = {}): any {
|
|
198
|
+
const body = fetch.mock.calls[0][1]?.body;
|
|
199
|
+
if (body instanceof URLSearchParams) return body.toString();
|
|
200
|
+
if (raw) return body;
|
|
201
|
+
return JSON.parse(String(body));
|
|
202
|
+
}`;
|
|
203
|
+
}
|