@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.
Files changed (73) hide show
  1. package/.github/workflows/ci.yml +20 -0
  2. package/.github/workflows/lint-pr-title.yml +16 -0
  3. package/.github/workflows/lint.yml +21 -0
  4. package/.github/workflows/release-please.yml +28 -0
  5. package/.github/workflows/release.yml +32 -0
  6. package/.husky/commit-msg +1 -0
  7. package/.husky/pre-commit +1 -0
  8. package/.husky/pre-push +1 -0
  9. package/.node-version +1 -0
  10. package/.oxfmtrc.json +10 -0
  11. package/.oxlintrc.json +29 -0
  12. package/.vscode/settings.json +11 -0
  13. package/LICENSE.txt +21 -0
  14. package/README.md +123 -0
  15. package/commitlint.config.ts +1 -0
  16. package/dist/index.d.ts +5 -0
  17. package/dist/index.js +2158 -0
  18. package/docs/endpoint-coverage.md +275 -0
  19. package/docs/sdk-architecture/node.md +355 -0
  20. package/oagen.config.ts +51 -0
  21. package/package.json +83 -0
  22. package/renovate.json +26 -0
  23. package/smoke/sdk-dotnet.ts +903 -0
  24. package/smoke/sdk-elixir.ts +771 -0
  25. package/smoke/sdk-go.ts +948 -0
  26. package/smoke/sdk-kotlin.ts +799 -0
  27. package/smoke/sdk-node.ts +516 -0
  28. package/smoke/sdk-php.ts +699 -0
  29. package/smoke/sdk-python.ts +738 -0
  30. package/smoke/sdk-ruby.ts +723 -0
  31. package/smoke/sdk-rust.ts +774 -0
  32. package/src/compat/extractors/dotnet.ts +8 -0
  33. package/src/compat/extractors/elixir.ts +8 -0
  34. package/src/compat/extractors/go.ts +8 -0
  35. package/src/compat/extractors/kotlin.ts +8 -0
  36. package/src/compat/extractors/node.ts +8 -0
  37. package/src/compat/extractors/php.ts +8 -0
  38. package/src/compat/extractors/python.ts +8 -0
  39. package/src/compat/extractors/ruby.ts +8 -0
  40. package/src/compat/extractors/rust.ts +8 -0
  41. package/src/index.ts +1 -0
  42. package/src/node/client.ts +356 -0
  43. package/src/node/common.ts +203 -0
  44. package/src/node/config.ts +70 -0
  45. package/src/node/enums.ts +87 -0
  46. package/src/node/errors.ts +205 -0
  47. package/src/node/fixtures.ts +139 -0
  48. package/src/node/index.ts +57 -0
  49. package/src/node/manifest.ts +23 -0
  50. package/src/node/models.ts +323 -0
  51. package/src/node/naming.ts +96 -0
  52. package/src/node/resources.ts +380 -0
  53. package/src/node/serializers.ts +286 -0
  54. package/src/node/tests.ts +336 -0
  55. package/src/node/type-map.ts +56 -0
  56. package/src/node/utils.ts +164 -0
  57. package/test/compat/extractors/node.test.ts +145 -0
  58. package/test/fixtures/sample-sdk-node/package.json +7 -0
  59. package/test/fixtures/sample-sdk-node/src/client.ts +24 -0
  60. package/test/fixtures/sample-sdk-node/src/index.ts +4 -0
  61. package/test/fixtures/sample-sdk-node/src/models.ts +28 -0
  62. package/test/fixtures/sample-sdk-node/tsconfig.json +13 -0
  63. package/test/node/client.test.ts +165 -0
  64. package/test/node/enums.test.ts +128 -0
  65. package/test/node/errors.test.ts +65 -0
  66. package/test/node/models.test.ts +301 -0
  67. package/test/node/naming.test.ts +212 -0
  68. package/test/node/resources.test.ts +260 -0
  69. package/test/node/serializers.test.ts +206 -0
  70. package/test/node/type-map.test.ts +127 -0
  71. package/tsconfig.json +20 -0
  72. package/tsup.config.ts +8 -0
  73. 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
+ }