@workos/oagen-emitters 0.0.1 → 0.2.0

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 (41) hide show
  1. package/.github/workflows/release-please.yml +9 -1
  2. package/.husky/commit-msg +0 -0
  3. package/.husky/pre-commit +1 -0
  4. package/.husky/pre-push +1 -0
  5. package/.prettierignore +1 -0
  6. package/.release-please-manifest.json +3 -0
  7. package/.vscode/settings.json +3 -0
  8. package/CHANGELOG.md +54 -0
  9. package/README.md +2 -2
  10. package/dist/index.d.mts +7 -0
  11. package/dist/index.d.mts.map +1 -0
  12. package/dist/index.mjs +3522 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/package.json +14 -18
  15. package/release-please-config.json +11 -0
  16. package/src/node/client.ts +437 -204
  17. package/src/node/common.ts +74 -4
  18. package/src/node/config.ts +1 -0
  19. package/src/node/enums.ts +50 -6
  20. package/src/node/errors.ts +78 -3
  21. package/src/node/fixtures.ts +84 -15
  22. package/src/node/index.ts +2 -2
  23. package/src/node/manifest.ts +4 -2
  24. package/src/node/models.ts +195 -79
  25. package/src/node/naming.ts +16 -1
  26. package/src/node/resources.ts +721 -106
  27. package/src/node/serializers.ts +510 -52
  28. package/src/node/tests.ts +621 -105
  29. package/src/node/type-map.ts +89 -11
  30. package/src/node/utils.ts +377 -114
  31. package/test/node/client.test.ts +979 -15
  32. package/test/node/enums.test.ts +0 -1
  33. package/test/node/errors.test.ts +4 -21
  34. package/test/node/models.test.ts +409 -2
  35. package/test/node/naming.test.ts +0 -3
  36. package/test/node/resources.test.ts +964 -7
  37. package/test/node/serializers.test.ts +212 -3
  38. package/tsconfig.json +2 -3
  39. package/{tsup.config.ts → tsdown.config.ts} +1 -1
  40. package/dist/index.d.ts +0 -5
  41. package/dist/index.js +0 -2158
@@ -12,7 +12,7 @@ export function generateCommon(): GeneratedFile[] {
12
12
  path: 'src/common/utils/fetch-and-deserialize.ts',
13
13
  content: fetchAndDeserializeContent(),
14
14
  skipIfExists: true,
15
- integrateTarget: false,
15
+ integrateTarget: true,
16
16
  },
17
17
  {
18
18
  path: 'src/common/serializers/list.serializer.ts',
@@ -24,7 +24,7 @@ export function generateCommon(): GeneratedFile[] {
24
24
  path: 'src/common/utils/test-utils.ts',
25
25
  content: testUtilsContent(),
26
26
  skipIfExists: true,
27
- integrateTarget: false,
27
+ integrateTarget: true,
28
28
  },
29
29
  ];
30
30
  }
@@ -108,7 +108,7 @@ export class AutoPaginatable<
108
108
  function fetchAndDeserializeContent(): string {
109
109
  return `import type { WorkOS } from '../../workos';
110
110
  import type { PaginationOptions } from '../interfaces/pagination-options.interface';
111
- import type { List, ListResponse } from './pagination';
111
+ import { AutoPaginatable, type List, type ListResponse } from './pagination';
112
112
 
113
113
  function setDefaultOptions(
114
114
  options?: PaginationOptions,
@@ -142,7 +142,20 @@ export const fetchAndDeserialize = async <T, U>(
142
142
  query: setDefaultOptions(options),
143
143
  });
144
144
  return deserializeList(data, deserializeFn);
145
- };`;
145
+ };
146
+
147
+ export async function createPaginatedList<TResponse, TModel, TOptions extends PaginationOptions>(
148
+ workos: WorkOS,
149
+ endpoint: string,
150
+ deserializeFn: (r: TResponse) => TModel,
151
+ options?: TOptions,
152
+ ): Promise<AutoPaginatable<TModel, TOptions>> {
153
+ return new AutoPaginatable(
154
+ await fetchAndDeserialize<TResponse, TModel>(workos, endpoint, deserializeFn, options),
155
+ (params) => fetchAndDeserialize<TResponse, TModel>(workos, endpoint, deserializeFn, params),
156
+ options,
157
+ );
158
+ }`;
146
159
  }
147
160
 
148
161
  function listSerializerContent(): string {
@@ -185,6 +198,10 @@ export function fetchURL(): string {
185
198
  return String(fetch.mock.calls[0][0]);
186
199
  }
187
200
 
201
+ export function fetchMethod(): string {
202
+ return String(fetch.mock.calls[0][1]?.method ?? 'GET');
203
+ }
204
+
188
205
  export function fetchSearchParams(): Record<string, string> {
189
206
  return Object.fromEntries(new URL(fetchURL()).searchParams);
190
207
  }
@@ -199,5 +216,58 @@ export function fetchBody({ raw = false } = {}): any {
199
216
  if (body instanceof URLSearchParams) return body.toString();
200
217
  if (raw) return body;
201
218
  return JSON.parse(String(body));
219
+ }
220
+
221
+ /**
222
+ * Shared test helper: asserts that the given async function throws when the
223
+ * server responds with 401 Unauthorized.
224
+ */
225
+ export function testUnauthorized(fn: () => Promise<any>) {
226
+ it('throws on unauthorized', async () => {
227
+ fetchOnce({ message: 'Unauthorized' }, { status: 401 });
228
+ await expect(fn()).rejects.toThrow('Unauthorized');
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Shared test helper: asserts that a paginated list call returns the expected
234
+ * shape (data array + listMetadata) and hits the correct endpoint.
235
+ */
236
+ export function testPaginatedList(
237
+ fn: () => Promise<any>,
238
+ pathContains: string,
239
+ ) {
240
+ it('returns paginated results', async () => {
241
+ // Caller must have called fetchOnce with the list fixture before invoking fn
242
+ const { data, listMetadata } = await fn();
243
+ expect(fetchURL()).toContain(pathContains);
244
+ expect(fetchSearchParams()).toHaveProperty('order');
245
+ expect(Array.isArray(data)).toBe(true);
246
+ expect(listMetadata).toBeDefined();
247
+ });
248
+ }
249
+
250
+ /**
251
+ * Shared test helper: asserts that a paginated list call returns empty data
252
+ * when the server responds with an empty list.
253
+ */
254
+ export function testEmptyResults(fn: () => Promise<any>) {
255
+ it('handles empty results', async () => {
256
+ fetchOnce({ data: [], list_metadata: { before: null, after: null } });
257
+ const { data } = await fn();
258
+ expect(data).toEqual([]);
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Shared test helper: asserts that pagination params are forwarded correctly.
264
+ */
265
+ export function testPaginationParams(fn: (opts: any) => Promise<any>, fixture: any) {
266
+ it('forwards pagination params', async () => {
267
+ fetchOnce(fixture);
268
+ await fn({ limit: 10, after: 'cursor_abc' });
269
+ expect(fetchSearchParams()['limit']).toBe('10');
270
+ expect(fetchSearchParams()['after']).toBe('cursor_abc');
271
+ });
202
272
  }`;
203
273
  }
@@ -29,6 +29,7 @@ export interface AppInfo {
29
29
  idempotencyKey?: string;
30
30
  warrantToken?: string;
31
31
  skipApiKeyCheck?: boolean;
32
+ encoding?: 'json' | 'form-data' | 'form-urlencoded' | 'binary' | 'text';
32
33
  }`,
33
34
  skipIfExists: true,
34
35
  integrateTarget: false,
package/src/node/enums.ts CHANGED
@@ -21,17 +21,44 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
21
21
  const baselineAlias = ctx.apiSurface?.typeAliases?.[enumDef.name];
22
22
  const lines: string[] = [];
23
23
 
24
+ // Track whether the generated content has new values not in the baseline.
25
+ // When it does, skipIfExists must be false so the file gets updated.
26
+ let hasNewValues = false;
27
+
24
28
  if (baselineEnum?.members) {
25
- // Generate TS `enum` using baseline member names and values directly
29
+ // Generate TS `enum` using baseline member names and values, merging
30
+ // any new IR values that the baseline is missing.
31
+ const existingValues = new Set(Object.values(baselineEnum.members).map(String));
32
+ const irValues = enumDef.values.map((v) => String(v.value));
33
+ const missingValues = irValues.filter((v) => !existingValues.has(v));
34
+ hasNewValues = missingValues.length > 0;
35
+
26
36
  lines.push(`export enum ${enumDef.name} {`);
27
37
  for (const [memberName, memberValue] of Object.entries(baselineEnum.members)) {
28
38
  const valueStr = typeof memberValue === 'string' ? `'${memberValue}'` : String(memberValue);
29
39
  lines.push(` ${memberName} = ${valueStr},`);
30
40
  }
41
+ // Append new values from the spec that the baseline is missing
42
+ for (const val of missingValues) {
43
+ // Derive a PascalCase member name from the value
44
+ const memberName = val.replace(/[^a-zA-Z0-9]+/g, '');
45
+ lines.push(` ${memberName} = '${val}',`);
46
+ }
31
47
  lines.push('}');
32
48
  } 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};`);
49
+ // Use the baseline type alias value, but merge in any new IR values the baseline is missing.
50
+ const baselineValues = extractLiteralUnionValues(baselineAlias.value);
51
+ const irValues = enumDef.values.map((v) => String(v.value));
52
+ const missing = irValues.filter((v) => !baselineValues.has(v));
53
+ hasNewValues = missing.length > 0;
54
+ if (missing.length > 0) {
55
+ // Baseline is missing values from the spec — regenerate with all values merged
56
+ const allValues = [...baselineValues, ...missing];
57
+ const parts = allValues.map((v) => `'${v}'`);
58
+ lines.push(`export type ${enumDef.name} = ${parts.join(' | ')};`);
59
+ } else {
60
+ lines.push(`export type ${enumDef.name} = ${baselineAlias.value};`);
61
+ }
35
62
  } else {
36
63
  // No baseline — generate string literal union from IR values
37
64
  const values = enumDef.values;
@@ -53,14 +80,31 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
53
80
  files.push({
54
81
  path: `src/${dirName}/interfaces/${fileName(enumDef.name)}.interface.ts`,
55
82
  content: lines.join('\n'),
56
- skipIfExists: true,
83
+ // When the spec has new values the baseline is missing, allow the file
84
+ // to be updated so the SDK picks up the full set of enum values.
85
+ skipIfExists: !hasNewValues,
57
86
  });
58
87
  }
59
88
 
60
89
  return files;
61
90
  }
62
91
 
63
- function assignEnumsToServices(enums: Enum[], services: Service[]): Map<string, string> {
92
+ /**
93
+ * Parse a TypeScript string literal union type alias value (e.g., "'a' | 'b' | 'c'")
94
+ * into a set of its string values.
95
+ */
96
+ function extractLiteralUnionValues(aliasValue: string): Set<string> {
97
+ const values = new Set<string>();
98
+ // Match all single-quoted string literals in the union
99
+ const regex = /'([^']+)'/g;
100
+ let match;
101
+ while ((match = regex.exec(aliasValue)) !== null) {
102
+ values.add(match[1]);
103
+ }
104
+ return values;
105
+ }
106
+
107
+ export function assignEnumsToServices(enums: Enum[], services: Service[]): Map<string, string> {
64
108
  const enumToService = new Map<string, string>();
65
109
  const enumNames = new Set(enums.map((e) => e.name));
66
110
 
@@ -72,7 +116,7 @@ function assignEnumsToServices(enums: Enum[], services: Service[]): Map<string,
72
116
  };
73
117
  if (op.requestBody) collect(op.requestBody);
74
118
  collect(op.response);
75
- for (const p of [...op.pathParams, ...op.queryParams, ...op.headerParams]) {
119
+ for (const p of [...op.pathParams, ...op.queryParams, ...op.headerParams, ...(op.cookieParams ?? [])]) {
76
120
  collect(p.type);
77
121
  }
78
122
  for (const name of refs) {
@@ -1,7 +1,8 @@
1
- import type { GeneratedFile } from '@workos/oagen';
1
+ import type { EmitterContext, GeneratedFile } from '@workos/oagen';
2
+ import { fileName } from './naming.js';
2
3
 
3
- export function generateErrors(): GeneratedFile[] {
4
- return [
4
+ export function generateErrors(ctx?: EmitterContext): GeneratedFile[] {
5
+ const files: GeneratedFile[] = [
5
6
  {
6
7
  path: 'src/common/exceptions/bad-request.exception.ts',
7
8
  content: `export class BadRequestException extends Error {
@@ -202,4 +203,78 @@ export { NoApiKeyProvidedException } from './no-api-key-provided.exception';`,
202
203
  integrateTarget: false,
203
204
  },
204
205
  ];
206
+
207
+ // Fix 6: Generate typed exception classes from ErrorResponse.type
208
+ if (ctx?.spec) {
209
+ const typedErrors = collectTypedErrors(ctx);
210
+ for (const { modelName, statusCode, baseException } of typedErrors) {
211
+ const exceptionClassName = `${modelName}Exception`;
212
+ const filePath = `src/common/exceptions/${fileName(modelName)}.exception.ts`;
213
+ const baseImport = baseException
214
+ ? `import { ${baseException} } from './${fileName(baseException.replace(/Exception$/, '')).replace(/^/, '')}.exception';\n\n`
215
+ : '';
216
+ const baseClass = baseException ?? 'Error';
217
+
218
+ files.push({
219
+ path: filePath,
220
+ content: `${baseImport}export class ${exceptionClassName} extends ${baseClass} {
221
+ readonly status = ${statusCode};
222
+ readonly name = '${exceptionClassName}';
223
+ readonly requestID: string;
224
+ readonly data?: any;
225
+
226
+ constructor({ message, requestID, data }: { message?: string; requestID: string; data?: any }) {
227
+ ${baseException ? `super(${baseException === 'UnauthorizedException' ? 'requestID' : `{ message: message ?? '${modelName} error', requestID }`});` : 'super();'}
228
+ this.message = message ?? '${modelName} error';
229
+ this.requestID = requestID;
230
+ if (data) this.data = data;
231
+ }
232
+ }`,
233
+ skipIfExists: true,
234
+ integrateTarget: false,
235
+ });
236
+
237
+ // Update the barrel index to export the new exception
238
+ const existingIndex = files.find((f) => f.path === 'src/common/exceptions/index.ts');
239
+ if (existingIndex) {
240
+ existingIndex.content += `\nexport { ${exceptionClassName} } from './${fileName(modelName)}.exception';`;
241
+ }
242
+ }
243
+ }
244
+
245
+ return files;
246
+ }
247
+
248
+ const STATUS_TO_BASE_EXCEPTION: Record<number, string> = {
249
+ 400: 'BadRequestException',
250
+ 401: 'UnauthorizedException',
251
+ 404: 'NotFoundException',
252
+ 409: 'ConflictException',
253
+ 422: 'UnprocessableEntityException',
254
+ 429: 'RateLimitExceededException',
255
+ };
256
+
257
+ function collectTypedErrors(
258
+ ctx: EmitterContext,
259
+ ): { modelName: string; statusCode: number; baseException: string | null }[] {
260
+ const seen = new Set<string>();
261
+ const results: { modelName: string; statusCode: number; baseException: string | null }[] = [];
262
+
263
+ for (const service of ctx.spec.services) {
264
+ for (const op of service.operations) {
265
+ for (const err of op.errors) {
266
+ if (err.type?.kind === 'model' && !seen.has(err.type.name)) {
267
+ seen.add(err.type.name);
268
+ results.push({
269
+ modelName: err.type.name,
270
+ statusCode: err.statusCode,
271
+ baseException:
272
+ STATUS_TO_BASE_EXCEPTION[err.statusCode] ?? (err.statusCode >= 500 ? 'GenericServerException' : null),
273
+ });
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ return results;
205
280
  }
@@ -1,6 +1,28 @@
1
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';
2
+ import { wireFieldName, fileName, serviceDirName } from './naming.js';
3
+ import { resolveResourceClassName } from './resources.js';
4
+ import { createServiceDirResolver, assignModelsToServices, isListMetadataModel, isListWrapperModel } from './utils.js';
5
+
6
+ /**
7
+ * Prefix mapping for generating realistic ID fixture values.
8
+ * When a field named "id" belongs to a model whose name matches a key here,
9
+ * the generated ID will be prefixed accordingly (e.g. "conn_01234").
10
+ */
11
+ export const ID_PREFIXES: Record<string, string> = {
12
+ Connection: 'conn_',
13
+ Organization: 'org_',
14
+ OrganizationMembership: 'om_',
15
+ User: 'user_',
16
+ Directory: 'directory_',
17
+ DirectoryGroup: 'dir_grp_',
18
+ DirectoryUser: 'dir_usr_',
19
+ Invitation: 'inv_',
20
+ Session: 'session_',
21
+ AuthenticationFactor: 'auth_factor_',
22
+ EmailVerification: 'email_verification_',
23
+ MagicAuth: 'magic_auth_',
24
+ PasswordReset: 'password_reset_',
25
+ };
4
26
 
5
27
  /**
6
28
  * Generate JSON fixture files for test data.
@@ -16,15 +38,21 @@ export function generateFixtures(
16
38
  ): { path: string; content: string }[] {
17
39
  if (spec.models.length === 0) return [];
18
40
 
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';
41
+ const { modelToService, resolveDir } = ctx
42
+ ? createServiceDirResolver(spec.models, ctx.spec.services, ctx)
43
+ : {
44
+ modelToService: assignModelsToServices(spec.models, spec.services),
45
+ resolveDir: (irService: string | undefined) => (irService ? serviceDirName(irService) : 'common'),
46
+ };
23
47
  const modelMap = new Map(spec.models.map((m) => [m.name, m]));
24
48
  const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
25
49
  const files: { path: string; content: string }[] = [];
26
50
 
27
51
  for (const model of spec.models) {
52
+ // Skip redundant list-metadata and list-wrapper models (handled by shared types)
53
+ if (isListMetadataModel(model)) continue;
54
+ if (isListWrapperModel(model)) continue;
55
+
28
56
  const service = modelToService.get(model.name);
29
57
  const dirName = resolveDir(service);
30
58
  const fixture = generateModelFixture(model, modelMap, enumMap);
@@ -37,12 +65,18 @@ export function generateFixtures(
37
65
 
38
66
  // Generate list fixtures for models that appear in paginated responses
39
67
  for (const service of spec.services) {
40
- const resolvedName = ctx ? resolveServiceName(service, ctx) : service.name;
68
+ const resolvedName = ctx ? resolveResourceClassName(service, ctx) : service.name;
41
69
  const serviceDir = serviceDirName(resolvedName);
42
70
  for (const op of service.operations) {
43
71
  if (op.pagination) {
44
- const itemModel = op.pagination.itemType.kind === 'model' ? modelMap.get(op.pagination.itemType.name) : null;
72
+ let itemModel = op.pagination.itemType.kind === 'model' ? modelMap.get(op.pagination.itemType.name) : null;
45
73
  if (itemModel) {
74
+ // Detect if the "item" model is actually a list wrapper (has `data` array + `list_metadata`).
75
+ // If so, unwrap to the actual item type to avoid double-nesting in fixtures.
76
+ const unwrapped = unwrapListModel(itemModel, modelMap);
77
+ if (unwrapped) {
78
+ itemModel = unwrapped;
79
+ }
46
80
  const fixture = generateModelFixture(itemModel, modelMap, enumMap);
47
81
  const listFixture = {
48
82
  data: [fixture],
@@ -63,6 +97,24 @@ export function generateFixtures(
63
97
  return files;
64
98
  }
65
99
 
100
+ /**
101
+ * Detect if a model is a list wrapper (has a `data` array field and a `list_metadata` field).
102
+ * If so, return the inner item model from the `data` array. Otherwise return null.
103
+ * This prevents double-nesting when the pagination itemType points to a list wrapper
104
+ * instead of the actual item model.
105
+ */
106
+ export function unwrapListModel(model: Model, modelMap: Map<string, Model>): Model | null {
107
+ const dataField = model.fields.find((f) => f.name === 'data');
108
+ const hasListMetadata = model.fields.some((f) => f.name === 'list_metadata' || f.name === 'listMetadata');
109
+ if (dataField && hasListMetadata && dataField.type.kind === 'array') {
110
+ const itemType = dataField.type.items;
111
+ if (itemType.kind === 'model') {
112
+ return modelMap.get(itemType.name) ?? null;
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+
66
118
  function generateModelFixture(
67
119
  model: Model,
68
120
  modelMap: Map<string, Model>,
@@ -72,7 +124,12 @@ function generateModelFixture(
72
124
 
73
125
  for (const field of model.fields) {
74
126
  const wireName = wireFieldName(field.name);
75
- fixture[wireName] = generateFieldValue(field.type, field.name, modelMap, enumMap);
127
+ // Prefer the OpenAPI example value when available on the field
128
+ if (field.example !== undefined) {
129
+ fixture[wireName] = field.example;
130
+ } else {
131
+ fixture[wireName] = generateFieldValue(field.type, field.name, model.name, modelMap, enumMap);
132
+ }
76
133
  }
77
134
 
78
135
  return fixture;
@@ -81,12 +138,13 @@ function generateModelFixture(
81
138
  function generateFieldValue(
82
139
  ref: TypeRef,
83
140
  fieldName: string,
141
+ modelName: string,
84
142
  modelMap: Map<string, Model>,
85
143
  enumMap: Map<string, Enum>,
86
144
  ): any {
87
145
  switch (ref.kind) {
88
146
  case 'primitive':
89
- return generatePrimitiveValue(ref.type, ref.format, fieldName);
147
+ return generatePrimitiveValue(ref.type, ref.format, fieldName, modelName);
90
148
  case 'literal':
91
149
  return ref.value;
92
150
  case 'enum': {
@@ -99,27 +157,38 @@ function generateFieldValue(
99
157
  return {};
100
158
  }
101
159
  case 'array': {
102
- const item = generateFieldValue(ref.items, fieldName, modelMap, enumMap);
160
+ // For array<enum>, use actual enum values instead of a single generated item
161
+ if (ref.items.kind === 'enum') {
162
+ const e = enumMap.get(ref.items.name);
163
+ if (e && e.values.length > 0) {
164
+ return e.values.map((v) => v.value);
165
+ }
166
+ }
167
+ const item = generateFieldValue(ref.items, fieldName, modelName, modelMap, enumMap);
103
168
  return [item];
104
169
  }
105
170
  case 'nullable':
106
- return generateFieldValue(ref.inner, fieldName, modelMap, enumMap);
171
+ return generateFieldValue(ref.inner, fieldName, modelName, modelMap, enumMap);
107
172
  case 'union':
108
173
  if (ref.variants.length > 0) {
109
- return generateFieldValue(ref.variants[0], fieldName, modelMap, enumMap);
174
+ return generateFieldValue(ref.variants[0], fieldName, modelName, modelMap, enumMap);
110
175
  }
111
176
  return null;
112
177
  case 'map':
113
- return { key: generateFieldValue(ref.valueType, 'value', modelMap, enumMap) };
178
+ return { key: generateFieldValue(ref.valueType, 'value', modelName, modelMap, enumMap) };
114
179
  }
115
180
  }
116
181
 
117
- function generatePrimitiveValue(type: string, format: string | undefined, name: string): any {
182
+ function generatePrimitiveValue(type: string, format: string | undefined, name: string, modelName: string): any {
118
183
  switch (type) {
119
184
  case 'string':
120
185
  if (format === 'date-time') return '2023-01-01T00:00:00.000Z';
121
186
  if (format === 'date') return '2023-01-01';
122
187
  if (format === 'uuid') return '00000000-0000-0000-0000-000000000000';
188
+ if (name === 'id') {
189
+ const prefix = ID_PREFIXES[modelName] ?? '';
190
+ return `${prefix}01234`;
191
+ }
123
192
  if (name.includes('id')) return `${name}_01234`;
124
193
  if (name.includes('email')) return 'test@example.com';
125
194
  if (name.includes('url') || name.includes('uri')) return 'https://example.com';
package/src/node/index.ts CHANGED
@@ -30,8 +30,8 @@ export const nodeEmitter: Emitter = {
30
30
  return generateClient(spec, ctx);
31
31
  },
32
32
 
33
- generateErrors(_ctx: EmitterContext): GeneratedFile[] {
34
- return generateErrors();
33
+ generateErrors(ctx: EmitterContext): GeneratedFile[] {
34
+ return generateErrors(ctx);
35
35
  },
36
36
 
37
37
  generateConfig(_ctx: EmitterContext): GeneratedFile[] {
@@ -1,11 +1,12 @@
1
1
  import type { ApiSpec, EmitterContext, GeneratedFile } from '@workos/oagen';
2
- import { resolveMethodName, servicePropertyName, resolveServiceName } from './naming.js';
2
+ import { resolveMethodName, servicePropertyName } from './naming.js';
3
+ import { resolveResourceClassName } from './resources.js';
3
4
 
4
5
  export function generateManifest(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
5
6
  const manifest: Record<string, { sdkMethod: string; service: string }> = {};
6
7
 
7
8
  for (const service of spec.services) {
8
- const propName = servicePropertyName(resolveServiceName(service, ctx));
9
+ const propName = servicePropertyName(resolveResourceClassName(service, ctx));
9
10
  for (const op of service.operations) {
10
11
  const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
11
12
  const method = resolveMethodName(op, service, ctx);
@@ -18,6 +19,7 @@ export function generateManifest(spec: ApiSpec, ctx: EmitterContext): GeneratedF
18
19
  path: 'smoke-manifest.json',
19
20
  content: JSON.stringify(manifest, null, 2),
20
21
  integrateTarget: false,
22
+ overwriteExisting: true,
21
23
  },
22
24
  ];
23
25
  }