@workos/oagen-emitters 0.2.1 → 0.3.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.
- package/.husky/pre-commit +1 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +8 -0
- package/README.md +129 -0
- package/dist/index.d.mts +10 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +11893 -3226
- package/dist/index.mjs.map +1 -1
- package/docs/sdk-architecture/go.md +338 -0
- package/docs/sdk-architecture/php.md +315 -0
- package/docs/sdk-architecture/python.md +511 -0
- package/oagen.config.ts +298 -2
- package/package.json +9 -5
- package/scripts/generate-php.js +13 -0
- package/scripts/git-push-with-published-oagen.sh +21 -0
- package/smoke/sdk-go.ts +116 -42
- package/smoke/sdk-php.ts +28 -26
- package/smoke/sdk-python.ts +5 -2
- package/src/go/client.ts +141 -0
- package/src/go/enums.ts +196 -0
- package/src/go/fixtures.ts +212 -0
- package/src/go/index.ts +81 -0
- package/src/go/manifest.ts +36 -0
- package/src/go/models.ts +254 -0
- package/src/go/naming.ts +191 -0
- package/src/go/resources.ts +827 -0
- package/src/go/tests.ts +751 -0
- package/src/go/type-map.ts +82 -0
- package/src/go/wrappers.ts +261 -0
- package/src/index.ts +3 -0
- package/src/node/client.ts +78 -115
- package/src/node/enums.ts +9 -0
- package/src/node/errors.ts +37 -232
- package/src/node/field-plan.ts +726 -0
- package/src/node/fixtures.ts +9 -1
- package/src/node/index.ts +2 -9
- package/src/node/models.ts +178 -21
- package/src/node/naming.ts +49 -111
- package/src/node/resources.ts +374 -364
- package/src/node/sdk-errors.ts +41 -0
- package/src/node/tests.ts +32 -12
- package/src/node/type-map.ts +4 -2
- package/src/node/utils.ts +13 -71
- package/src/node/wrappers.ts +151 -0
- package/src/php/client.ts +171 -0
- package/src/php/enums.ts +67 -0
- package/src/php/errors.ts +9 -0
- package/src/php/fixtures.ts +181 -0
- package/src/php/index.ts +96 -0
- package/src/php/manifest.ts +36 -0
- package/src/php/models.ts +310 -0
- package/src/php/naming.ts +298 -0
- package/src/php/resources.ts +561 -0
- package/src/php/tests.ts +533 -0
- package/src/php/type-map.ts +90 -0
- package/src/php/utils.ts +18 -0
- package/src/php/wrappers.ts +151 -0
- package/src/python/client.ts +337 -0
- package/src/python/enums.ts +313 -0
- package/src/python/fixtures.ts +196 -0
- package/src/python/index.ts +95 -0
- package/src/python/manifest.ts +38 -0
- package/src/python/models.ts +688 -0
- package/src/python/naming.ts +209 -0
- package/src/python/resources.ts +1322 -0
- package/src/python/tests.ts +1335 -0
- package/src/python/type-map.ts +93 -0
- package/src/python/wrappers.ts +191 -0
- package/src/shared/model-utils.ts +255 -0
- package/src/shared/naming-utils.ts +107 -0
- package/src/shared/non-spec-services.ts +54 -0
- package/src/shared/resolved-ops.ts +109 -0
- package/src/shared/wrapper-utils.ts +59 -0
- package/test/go/client.test.ts +92 -0
- package/test/go/enums.test.ts +132 -0
- package/test/go/errors.test.ts +9 -0
- package/test/go/models.test.ts +265 -0
- package/test/go/resources.test.ts +408 -0
- package/test/go/tests.test.ts +143 -0
- package/test/node/client.test.ts +18 -12
- package/test/node/enums.test.ts +2 -0
- package/test/node/errors.test.ts +2 -41
- package/test/node/models.test.ts +2 -0
- package/test/node/naming.test.ts +23 -0
- package/test/node/resources.test.ts +99 -69
- package/test/node/serializers.test.ts +3 -1
- package/test/node/type-map.test.ts +11 -0
- package/test/php/client.test.ts +94 -0
- package/test/php/enums.test.ts +173 -0
- package/test/php/errors.test.ts +9 -0
- package/test/php/models.test.ts +497 -0
- package/test/php/resources.test.ts +644 -0
- package/test/php/tests.test.ts +118 -0
- package/test/python/client.test.ts +200 -0
- package/test/python/enums.test.ts +228 -0
- package/test/python/errors.test.ts +16 -0
- package/test/python/manifest.test.ts +74 -0
- package/test/python/models.test.ts +716 -0
- package/test/python/resources.test.ts +617 -0
- package/test/python/tests.test.ts +202 -0
- package/src/node/common.ts +0 -273
- package/src/node/config.ts +0 -71
- package/src/node/serializers.ts +0 -746
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { TypeRef, PrimitiveType, UnionType } from '@workos/oagen';
|
|
2
|
+
import { mapTypeRef as irMapTypeRef } from '@workos/oagen';
|
|
3
|
+
import { className } from './naming.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Map an IR TypeRef to a Go type string.
|
|
7
|
+
*/
|
|
8
|
+
export function mapTypeRef(ref: TypeRef, asPointer = false): string {
|
|
9
|
+
const base = irMapTypeRef<string>(ref, {
|
|
10
|
+
primitive: mapPrimitive,
|
|
11
|
+
array: (_ref, items) => `[]${items}`,
|
|
12
|
+
model: (r) => `*${className(r.name)}`,
|
|
13
|
+
enum: (r) => className(r.name),
|
|
14
|
+
union: (_r, variants) => joinUnionVariants(_r, variants),
|
|
15
|
+
nullable: (_ref, inner) => {
|
|
16
|
+
// If inner is already a pointer type (model), don't double-pointer
|
|
17
|
+
if (inner.startsWith('*')) return inner;
|
|
18
|
+
return `*${inner}`;
|
|
19
|
+
},
|
|
20
|
+
literal: (r) => {
|
|
21
|
+
if (r.value === null) return 'interface{}';
|
|
22
|
+
if (typeof r.value === 'string') return 'string';
|
|
23
|
+
if (typeof r.value === 'number') return Number.isInteger(r.value) ? 'int' : 'float64';
|
|
24
|
+
if (typeof r.value === 'boolean') return 'bool';
|
|
25
|
+
return 'interface{}';
|
|
26
|
+
},
|
|
27
|
+
map: (_ref, value) => `map[string]${value}`,
|
|
28
|
+
});
|
|
29
|
+
if (asPointer && !base.startsWith('*') && !base.startsWith('[]') && !base.startsWith('map[')) {
|
|
30
|
+
return `*${base}`;
|
|
31
|
+
}
|
|
32
|
+
return base;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Map an IR TypeRef to a Go type string without pointer wrapping for models.
|
|
37
|
+
* Used for response type references where we don't want a double pointer.
|
|
38
|
+
*/
|
|
39
|
+
export function mapTypeRefValue(ref: TypeRef): string {
|
|
40
|
+
return irMapTypeRef<string>(ref, {
|
|
41
|
+
primitive: mapPrimitive,
|
|
42
|
+
array: (_ref, items) => `[]${items}`,
|
|
43
|
+
model: (r) => className(r.name),
|
|
44
|
+
enum: (r) => className(r.name),
|
|
45
|
+
union: (_r, variants) => joinUnionVariants(_r, variants),
|
|
46
|
+
nullable: (_ref, inner) => `*${inner}`,
|
|
47
|
+
literal: (r) => {
|
|
48
|
+
if (r.value === null) return 'interface{}';
|
|
49
|
+
if (typeof r.value === 'string') return 'string';
|
|
50
|
+
if (typeof r.value === 'number') return Number.isInteger(r.value) ? 'int' : 'float64';
|
|
51
|
+
if (typeof r.value === 'boolean') return 'bool';
|
|
52
|
+
return 'interface{}';
|
|
53
|
+
},
|
|
54
|
+
map: (_ref, value) => `map[string]${value}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mapPrimitive(ref: PrimitiveType): string {
|
|
59
|
+
if (ref.format === 'binary') return '[]byte';
|
|
60
|
+
switch (ref.type) {
|
|
61
|
+
case 'string':
|
|
62
|
+
return 'string';
|
|
63
|
+
case 'integer':
|
|
64
|
+
return 'int';
|
|
65
|
+
case 'number':
|
|
66
|
+
return 'float64';
|
|
67
|
+
case 'boolean':
|
|
68
|
+
return 'bool';
|
|
69
|
+
case 'unknown':
|
|
70
|
+
return 'interface{}';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function joinUnionVariants(_ref: UnionType, variants: string[]): string {
|
|
75
|
+
if (_ref.compositionKind === 'allOf') {
|
|
76
|
+
return variants[0] ?? 'interface{}';
|
|
77
|
+
}
|
|
78
|
+
const unique = [...new Set(variants)];
|
|
79
|
+
if (unique.length === 1) return unique[0];
|
|
80
|
+
// Go doesn't have union types; use interface{}
|
|
81
|
+
return 'interface{}';
|
|
82
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { EmitterContext, ResolvedOperation, ResolvedWrapper } from '@workos/oagen';
|
|
2
|
+
import {
|
|
3
|
+
className as goClassName,
|
|
4
|
+
fieldName as goFieldName,
|
|
5
|
+
methodName as goMethodName,
|
|
6
|
+
unexportedName,
|
|
7
|
+
} from './naming.js';
|
|
8
|
+
import { sortPathParamsByTemplateOrder } from './resources.js';
|
|
9
|
+
import { resolveWrapperParams, formatWrapperDescription, type ResolvedWrapperParam } from '../shared/wrapper-utils.js';
|
|
10
|
+
import { lowerFirstForDoc, fieldDocComment } from '../shared/naming-utils.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate Go wrapper method lines for union split operations.
|
|
14
|
+
*
|
|
15
|
+
* Each wrapper is a typed convenience method that:
|
|
16
|
+
* - Accepts only the exposed params (not the full union body)
|
|
17
|
+
* - Injects constant defaults (e.g., grant_type)
|
|
18
|
+
* - Reads inferred fields from client config (e.g., client_id)
|
|
19
|
+
* - Delegates to the HTTP client with the constructed body
|
|
20
|
+
*/
|
|
21
|
+
export function generateWrapperMethods(
|
|
22
|
+
serviceType: string,
|
|
23
|
+
resolvedOp: ResolvedOperation,
|
|
24
|
+
ctx: EmitterContext,
|
|
25
|
+
): string[] {
|
|
26
|
+
if (!resolvedOp.wrappers || resolvedOp.wrappers.length === 0) return [];
|
|
27
|
+
|
|
28
|
+
const lines: string[] = [];
|
|
29
|
+
|
|
30
|
+
for (const wrapper of resolvedOp.wrappers) {
|
|
31
|
+
const wrapperParams = resolveWrapperParams(wrapper, ctx);
|
|
32
|
+
lines.push('');
|
|
33
|
+
emitWrapperParamsStruct(lines, wrapper, wrapperParams);
|
|
34
|
+
lines.push('');
|
|
35
|
+
emitWrapperMethod(lines, serviceType, resolvedOp, wrapper, wrapperParams);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return lines;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function emitWrapperParamsStruct(
|
|
42
|
+
lines: string[],
|
|
43
|
+
wrapper: ResolvedWrapper,
|
|
44
|
+
wrapperParams: ResolvedWrapperParam[],
|
|
45
|
+
): void {
|
|
46
|
+
const structName = `${goMethodName(wrapper.name)}Params`;
|
|
47
|
+
|
|
48
|
+
lines.push(`// ${structName} contains the parameters for ${goMethodName(wrapper.name)}.`);
|
|
49
|
+
lines.push(`type ${structName} struct {`);
|
|
50
|
+
|
|
51
|
+
for (const { paramName, field, isOptional } of wrapperParams) {
|
|
52
|
+
const goField = goFieldName(paramName);
|
|
53
|
+
const goType = field ? resolveSimpleGoType(field.type) : 'string';
|
|
54
|
+
|
|
55
|
+
if (field?.description) {
|
|
56
|
+
const fdLines = field.description.split('\n').filter((l: string) => l.trim());
|
|
57
|
+
lines.push(`\t// ${fieldDocComment(goField, fdLines[0])}`);
|
|
58
|
+
for (let i = 1; i < fdLines.length; i++) {
|
|
59
|
+
lines.push(`\t// ${fdLines[i].trim()}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (isOptional) {
|
|
63
|
+
const optType = goType.startsWith('*') || goType.startsWith('[]') ? goType : `*${goType}`;
|
|
64
|
+
lines.push(`\t${goField} ${optType} \`json:"${paramName},omitempty"\``);
|
|
65
|
+
} else {
|
|
66
|
+
lines.push(`\t${goField} ${goType} \`json:"${paramName}"\``);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
lines.push('}');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function emitWrapperMethod(
|
|
74
|
+
lines: string[],
|
|
75
|
+
serviceType: string,
|
|
76
|
+
resolvedOp: ResolvedOperation,
|
|
77
|
+
wrapper: ResolvedWrapper,
|
|
78
|
+
wrapperParams: ResolvedWrapperParam[],
|
|
79
|
+
): void {
|
|
80
|
+
const op = resolvedOp.operation;
|
|
81
|
+
const method = goMethodName(wrapper.name);
|
|
82
|
+
const paramsStruct = `${method}Params`;
|
|
83
|
+
|
|
84
|
+
// Return type
|
|
85
|
+
const responseType = wrapper.responseModelName ? goClassName(wrapper.responseModelName) : null;
|
|
86
|
+
|
|
87
|
+
// GoDoc
|
|
88
|
+
lines.push(`// ${method} ${formatWrapperDescription(wrapper.name)}.`);
|
|
89
|
+
|
|
90
|
+
// Signature
|
|
91
|
+
const sigParams: string[] = ['ctx context.Context'];
|
|
92
|
+
|
|
93
|
+
// Path params as positional args (sorted by template order)
|
|
94
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
95
|
+
sigParams.push(`${lowerFirstSafe(goFieldName(p.name))} ${resolveSimpleGoType(p.type)}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
sigParams.push(`params *${paramsStruct}`);
|
|
99
|
+
sigParams.push('opts ...RequestOption');
|
|
100
|
+
|
|
101
|
+
if (responseType) {
|
|
102
|
+
lines.push(`func (s *${serviceType}) ${method}(${sigParams.join(', ')}) (*${responseType}, error) {`);
|
|
103
|
+
} else {
|
|
104
|
+
lines.push(`func (s *${serviceType}) ${method}(${sigParams.join(', ')}) error {`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Build body map with defaults + exposed params
|
|
108
|
+
lines.push('\tbody := map[string]interface{}{');
|
|
109
|
+
|
|
110
|
+
// Constant defaults (e.g., grant_type)
|
|
111
|
+
for (const [key, value] of Object.entries(wrapper.defaults)) {
|
|
112
|
+
lines.push(`\t\t"${key}": ${goLiteral(value)},`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Required exposed params
|
|
116
|
+
for (const { paramName, isOptional } of wrapperParams) {
|
|
117
|
+
if (isOptional) continue;
|
|
118
|
+
const goField = goFieldName(paramName);
|
|
119
|
+
lines.push(`\t\t"${paramName}": params.${goField},`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
lines.push('\t}');
|
|
123
|
+
|
|
124
|
+
// Inferred fields from client config
|
|
125
|
+
for (const field of wrapper.inferFromClient) {
|
|
126
|
+
const expr = clientFieldExpression(field);
|
|
127
|
+
lines.push(`\tif ${expr} != "" {`);
|
|
128
|
+
lines.push(`\t\tbody["${field}"] = ${expr}`);
|
|
129
|
+
lines.push('\t}');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Optional exposed params
|
|
133
|
+
for (const { paramName, isOptional } of wrapperParams) {
|
|
134
|
+
if (!isOptional) continue;
|
|
135
|
+
const goField = goFieldName(paramName);
|
|
136
|
+
lines.push(`\tif params.${goField} != nil {`);
|
|
137
|
+
lines.push(`\t\tbody["${paramName}"] = *params.${goField}`);
|
|
138
|
+
lines.push('\t}');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Build path expression
|
|
142
|
+
let pathExpr: string;
|
|
143
|
+
if (op.pathParams.length > 0) {
|
|
144
|
+
let fmtStr = op.path;
|
|
145
|
+
const fmtArgs: string[] = [];
|
|
146
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
147
|
+
fmtStr = fmtStr.replace(`{${p.name}}`, '%s');
|
|
148
|
+
fmtArgs.push(lowerFirstSafe(goFieldName(p.name)));
|
|
149
|
+
}
|
|
150
|
+
pathExpr = `fmt.Sprintf("${fmtStr}", ${fmtArgs.join(', ')})`;
|
|
151
|
+
} else {
|
|
152
|
+
pathExpr = `"${op.path}"`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Make the request
|
|
156
|
+
if (responseType) {
|
|
157
|
+
lines.push(`\tvar result ${responseType}`);
|
|
158
|
+
lines.push(
|
|
159
|
+
`\t_, err := s.client.request(ctx, "${op.httpMethod.toUpperCase()}", ${pathExpr}, nil, body, &result, opts)`,
|
|
160
|
+
);
|
|
161
|
+
lines.push('\tif err != nil {');
|
|
162
|
+
lines.push('\t\treturn nil, err');
|
|
163
|
+
lines.push('\t}');
|
|
164
|
+
lines.push('\treturn &result, nil');
|
|
165
|
+
} else {
|
|
166
|
+
lines.push(
|
|
167
|
+
`\t_, err := s.client.request(ctx, "${op.httpMethod.toUpperCase()}", ${pathExpr}, nil, body, nil, opts)`,
|
|
168
|
+
);
|
|
169
|
+
lines.push('\treturn err');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
lines.push('}');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Convert a value to a Go literal. */
|
|
176
|
+
function goLiteral(value: string | number | boolean): string {
|
|
177
|
+
if (typeof value === 'string') return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
178
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
179
|
+
return String(value);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Get the Go expression for reading a client config field. */
|
|
183
|
+
function clientFieldExpression(field: string): string {
|
|
184
|
+
switch (field) {
|
|
185
|
+
case 'client_id':
|
|
186
|
+
return 's.client.clientID';
|
|
187
|
+
case 'client_secret':
|
|
188
|
+
return 's.client.apiKey';
|
|
189
|
+
default:
|
|
190
|
+
return `s.client.${lowerFirstSafe(goFieldName(field))}`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Resolve a TypeRef to a simple Go type string. */
|
|
195
|
+
function resolveSimpleGoType(ref: any): string {
|
|
196
|
+
if (ref.kind === 'primitive') {
|
|
197
|
+
switch (ref.type) {
|
|
198
|
+
case 'string':
|
|
199
|
+
return 'string';
|
|
200
|
+
case 'integer':
|
|
201
|
+
return 'int';
|
|
202
|
+
case 'number':
|
|
203
|
+
return 'float64';
|
|
204
|
+
case 'boolean':
|
|
205
|
+
return 'bool';
|
|
206
|
+
default:
|
|
207
|
+
return 'interface{}';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (ref.kind === 'nullable') return `*${resolveSimpleGoType(ref.inner)}`;
|
|
211
|
+
if (ref.kind === 'array') return `[]${resolveSimpleGoType(ref.items)}`;
|
|
212
|
+
if (ref.kind === 'model') return `*${goClassName(ref.name)}`;
|
|
213
|
+
if (ref.kind === 'enum') return goClassName(ref.name);
|
|
214
|
+
if (ref.kind === 'union') {
|
|
215
|
+
// For oneOf with a single non-null variant, use that variant's type
|
|
216
|
+
const nonNull = ref.variants.filter((v: any) => v.kind !== 'literal' || v.value !== null);
|
|
217
|
+
if (nonNull.length === 1) return resolveSimpleGoType(nonNull[0]);
|
|
218
|
+
return 'interface{}';
|
|
219
|
+
}
|
|
220
|
+
return 'interface{}';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Go reserved words set. */
|
|
224
|
+
const GO_RESERVED = new Set([
|
|
225
|
+
'break',
|
|
226
|
+
'case',
|
|
227
|
+
'chan',
|
|
228
|
+
'const',
|
|
229
|
+
'continue',
|
|
230
|
+
'default',
|
|
231
|
+
'defer',
|
|
232
|
+
'else',
|
|
233
|
+
'fallthrough',
|
|
234
|
+
'for',
|
|
235
|
+
'func',
|
|
236
|
+
'go',
|
|
237
|
+
'goto',
|
|
238
|
+
'if',
|
|
239
|
+
'import',
|
|
240
|
+
'interface',
|
|
241
|
+
'map',
|
|
242
|
+
'package',
|
|
243
|
+
'range',
|
|
244
|
+
'return',
|
|
245
|
+
'select',
|
|
246
|
+
'struct',
|
|
247
|
+
'switch',
|
|
248
|
+
'type',
|
|
249
|
+
'var',
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
function lowerFirstSafe(s: string): string {
|
|
253
|
+
if (!s) return s;
|
|
254
|
+
const result = unexportedName(s);
|
|
255
|
+
if (GO_RESERVED.has(result)) return `${result}Param`;
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function _lowerFirstField(s: string): string {
|
|
260
|
+
return lowerFirstForDoc(s);
|
|
261
|
+
}
|
package/src/index.ts
CHANGED
package/src/node/client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ApiSpec, AuthScheme, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
|
|
2
|
+
import { collectReferencedNames } from '@workos/oagen';
|
|
2
3
|
import { fileName, resolveServiceDir, servicePropertyName, resolveInterfaceName, wireInterfaceName } from './naming.js';
|
|
3
4
|
import {
|
|
4
5
|
docComment,
|
|
@@ -15,9 +16,7 @@ export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFil
|
|
|
15
16
|
files.push(generateWorkOSClient(spec, ctx));
|
|
16
17
|
files.push(...generateServiceBarrels(spec, ctx));
|
|
17
18
|
files.push(generateBarrel(spec, ctx));
|
|
18
|
-
|
|
19
|
-
files.push(generatePackageJson(ctx));
|
|
20
|
-
files.push(generateTsConfig());
|
|
19
|
+
// worker barrel, package.json, tsconfig.json are now hand-maintained in the target SDK
|
|
21
20
|
|
|
22
21
|
return files;
|
|
23
22
|
}
|
|
@@ -128,47 +127,36 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
|
|
|
128
127
|
// exports a name (e.g., AuditLogSchema from create-audit-log-schema-options),
|
|
129
128
|
// the generated model with the same name must be skipped to prevent the
|
|
130
129
|
// merger from adding a duplicate `export *` that causes TS2308.
|
|
130
|
+
//
|
|
131
|
+
// Also track baseline file stems per directory so we can detect when the
|
|
132
|
+
// barrel needs updating with new export lines (see hasNewExports below).
|
|
133
|
+
const dirSymbolsFromBaseline = new Map<string, Set<string>>();
|
|
134
|
+
const seedFromBaseline = (sourceFile: string, name: string) => {
|
|
135
|
+
const match = sourceFile.match(/^src\/([^/]+)\/interfaces\/(.+)\.ts$/);
|
|
136
|
+
if (!match) return;
|
|
137
|
+
const dirName = match[1];
|
|
138
|
+
const fileStem = match[2];
|
|
139
|
+
if (!dirSymbols.has(dirName)) dirSymbols.set(dirName, new Set());
|
|
140
|
+
dirSymbols.get(dirName)!.add(name);
|
|
141
|
+
if (!dirSymbolsFromBaseline.has(dirName)) dirSymbolsFromBaseline.set(dirName, new Set());
|
|
142
|
+
dirSymbolsFromBaseline.get(dirName)!.add(fileStem);
|
|
143
|
+
};
|
|
131
144
|
if (ctx.apiSurface?.interfaces) {
|
|
132
145
|
for (const [name, iface] of Object.entries(ctx.apiSurface.interfaces)) {
|
|
133
146
|
const sourceFile = (iface as any).sourceFile as string | undefined;
|
|
134
|
-
if (
|
|
135
|
-
// Match paths like "src/audit-logs/interfaces/foo.interface.ts" to directory "audit-logs"
|
|
136
|
-
const match = sourceFile.match(/^src\/([^/]+)\/interfaces\//);
|
|
137
|
-
if (match) {
|
|
138
|
-
const dirName = match[1];
|
|
139
|
-
if (!dirSymbols.has(dirName)) {
|
|
140
|
-
dirSymbols.set(dirName, new Set());
|
|
141
|
-
}
|
|
142
|
-
dirSymbols.get(dirName)!.add(name);
|
|
143
|
-
}
|
|
147
|
+
if (sourceFile) seedFromBaseline(sourceFile, name);
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
150
|
if (ctx.apiSurface?.enums) {
|
|
147
151
|
for (const [name, enumDef] of Object.entries(ctx.apiSurface.enums)) {
|
|
148
152
|
const sourceFile = (enumDef as any).sourceFile as string | undefined;
|
|
149
|
-
if (
|
|
150
|
-
const match = sourceFile.match(/^src\/([^/]+)\/interfaces\//);
|
|
151
|
-
if (match) {
|
|
152
|
-
const dirName = match[1];
|
|
153
|
-
if (!dirSymbols.has(dirName)) {
|
|
154
|
-
dirSymbols.set(dirName, new Set());
|
|
155
|
-
}
|
|
156
|
-
dirSymbols.get(dirName)!.add(name);
|
|
157
|
-
}
|
|
153
|
+
if (sourceFile) seedFromBaseline(sourceFile, name);
|
|
158
154
|
}
|
|
159
155
|
}
|
|
160
156
|
if (ctx.apiSurface?.typeAliases) {
|
|
161
157
|
for (const [name, alias] of Object.entries(ctx.apiSurface.typeAliases)) {
|
|
162
158
|
const sourceFile = (alias as any).sourceFile as string | undefined;
|
|
163
|
-
if (
|
|
164
|
-
const match = sourceFile.match(/^src\/([^/]+)\/interfaces\//);
|
|
165
|
-
if (match) {
|
|
166
|
-
const dirName = match[1];
|
|
167
|
-
if (!dirSymbols.has(dirName)) {
|
|
168
|
-
dirSymbols.set(dirName, new Set());
|
|
169
|
-
}
|
|
170
|
-
dirSymbols.get(dirName)!.add(name);
|
|
171
|
-
}
|
|
159
|
+
if (sourceFile) seedFromBaseline(sourceFile, name);
|
|
172
160
|
}
|
|
173
161
|
}
|
|
174
162
|
|
|
@@ -187,8 +175,12 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
|
|
|
187
175
|
// Models -> service directories
|
|
188
176
|
// Skip list wrapper and list metadata models — they use shared List<T>/ListMetadata
|
|
189
177
|
// from common utils, so no per-resource interface file is generated.
|
|
178
|
+
// Also skip unreachable models — oagen only passes service-referenced models
|
|
179
|
+
// to generateModels, so unreachable models have no interface file to export.
|
|
180
|
+
const barrelReachable = collectReferencedNames(spec.services, spec.models);
|
|
190
181
|
for (const model of spec.models) {
|
|
191
182
|
if (isListMetadataModel(model) || isListWrapperModel(model)) continue;
|
|
183
|
+
if (!barrelReachable.models.has(model.name)) continue;
|
|
192
184
|
const service = modelToService.get(model.name);
|
|
193
185
|
const dirName = resolveDir(service);
|
|
194
186
|
if (!dirExports.has(dirName)) {
|
|
@@ -240,14 +232,57 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
|
|
|
240
232
|
}
|
|
241
233
|
|
|
242
234
|
for (const [dirName, exports] of dirExports) {
|
|
243
|
-
|
|
244
|
-
|
|
235
|
+
const exportSet = new Set(exports);
|
|
236
|
+
|
|
237
|
+
// When integrating into an existing SDK, include baseline exports from
|
|
238
|
+
// the api-surface so the barrel is comprehensive. This ensures stale
|
|
239
|
+
// entries (e.g., renamed files from previous generations) are removed
|
|
240
|
+
// when overwriteExisting replaces the barrel.
|
|
241
|
+
if (ctx.apiSurface) {
|
|
242
|
+
const addBaselineExports = (items: Record<string, any> | undefined) => {
|
|
243
|
+
if (!items) return;
|
|
244
|
+
for (const item of Object.values(items)) {
|
|
245
|
+
const sourceFile = (item as any).sourceFile as string | undefined;
|
|
246
|
+
if (!sourceFile) continue;
|
|
247
|
+
const match = sourceFile.match(/^src\/([^/]+)\/interfaces\/(.+)\.ts$/);
|
|
248
|
+
if (match && match[1] === dirName) {
|
|
249
|
+
exportSet.add(`export * from './${match[2].replace(/\.ts$/, '')}';`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
addBaselineExports(ctx.apiSurface.interfaces);
|
|
254
|
+
addBaselineExports(ctx.apiSurface.typeAliases);
|
|
255
|
+
addBaselineExports(ctx.apiSurface.enums);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Deduplicate and sort
|
|
259
|
+
const uniqueExports = [...exportSet];
|
|
245
260
|
uniqueExports.sort();
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
261
|
+
|
|
262
|
+
if (ctx.apiSurface) {
|
|
263
|
+
// Integration mode: overwrite the barrel so stale entries are removed.
|
|
264
|
+
files.push({
|
|
265
|
+
path: `src/${dirName}/interfaces/index.ts`,
|
|
266
|
+
content: uniqueExports.join('\n'),
|
|
267
|
+
overwriteExisting: true,
|
|
268
|
+
});
|
|
269
|
+
} else {
|
|
270
|
+
// Standalone generation: only update if there are new exports.
|
|
271
|
+
const baselineSymbols = dirSymbolsFromBaseline.get(dirName);
|
|
272
|
+
const hasNewExports = baselineSymbols
|
|
273
|
+
? uniqueExports.some((exp) => {
|
|
274
|
+
const match = exp.match(/from '\.\/(.*?)'/);
|
|
275
|
+
if (!match) return false;
|
|
276
|
+
return !baselineSymbols.has(match[1]);
|
|
277
|
+
})
|
|
278
|
+
: false;
|
|
279
|
+
|
|
280
|
+
files.push({
|
|
281
|
+
path: `src/${dirName}/interfaces/index.ts`,
|
|
282
|
+
content: uniqueExports.join('\n'),
|
|
283
|
+
skipIfExists: !hasNewExports,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
251
286
|
}
|
|
252
287
|
|
|
253
288
|
return files;
|
|
@@ -457,7 +492,11 @@ function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
|
|
|
457
492
|
}
|
|
458
493
|
|
|
459
494
|
// Unassigned models (common) — use barrel if any exist
|
|
460
|
-
|
|
495
|
+
// Filter to reachable models only: oagen's generateAllFiles passes only
|
|
496
|
+
// service-referenced models to generateModels, so unreachable models
|
|
497
|
+
// never get interface files. Exporting them here would create broken imports.
|
|
498
|
+
const reachable = collectReferencedNames(spec.services, spec.models);
|
|
499
|
+
const unassignedModels = spec.models.filter((m) => !modelToService.has(m.name) && reachable.models.has(m.name));
|
|
461
500
|
const commonEnums = spec.enums.filter((e) => {
|
|
462
501
|
const enumService = findEnumService(e.name, spec.services);
|
|
463
502
|
return !enumService;
|
|
@@ -527,23 +566,6 @@ function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
|
|
|
527
566
|
};
|
|
528
567
|
}
|
|
529
568
|
|
|
530
|
-
/**
|
|
531
|
-
* Generate a worker-compatible barrel file that re-exports everything from
|
|
532
|
-
* the main barrel. This keeps type exports in sync automatically.
|
|
533
|
-
*/
|
|
534
|
-
function generateWorkerBarrel(_spec: ApiSpec, _ctx: EmitterContext): GeneratedFile {
|
|
535
|
-
const lines: string[] = [];
|
|
536
|
-
|
|
537
|
-
// Re-export everything from the main index — keeps type exports in sync
|
|
538
|
-
lines.push("export * from './index';");
|
|
539
|
-
|
|
540
|
-
return {
|
|
541
|
-
path: 'src/index.worker.ts',
|
|
542
|
-
content: lines.join('\n'),
|
|
543
|
-
skipIfExists: true,
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
|
|
547
569
|
function findEnumService(enumName: string, services: Service[]): string | undefined {
|
|
548
570
|
for (const service of services) {
|
|
549
571
|
for (const op of service.operations) {
|
|
@@ -610,62 +632,3 @@ function serverConstName(description: string): string {
|
|
|
610
632
|
.toUpperCase()
|
|
611
633
|
);
|
|
612
634
|
}
|
|
613
|
-
|
|
614
|
-
function generatePackageJson(ctx: EmitterContext): GeneratedFile {
|
|
615
|
-
const pkg = {
|
|
616
|
-
name: `@${ctx.namespace}/sdk`,
|
|
617
|
-
version: '0.0.0',
|
|
618
|
-
type: 'module',
|
|
619
|
-
main: 'src/index.ts',
|
|
620
|
-
types: 'src/index.ts',
|
|
621
|
-
exports: {
|
|
622
|
-
'.': './src/index.ts',
|
|
623
|
-
},
|
|
624
|
-
scripts: {
|
|
625
|
-
test: 'jest',
|
|
626
|
-
build: 'tsc',
|
|
627
|
-
},
|
|
628
|
-
devDependencies: {
|
|
629
|
-
typescript: '^5.0.0',
|
|
630
|
-
jest: '^29.0.0',
|
|
631
|
-
'jest-fetch-mock': '^3.0.0',
|
|
632
|
-
'@types/jest': '^29.0.0',
|
|
633
|
-
'ts-jest': '^29.0.0',
|
|
634
|
-
},
|
|
635
|
-
};
|
|
636
|
-
|
|
637
|
-
return {
|
|
638
|
-
path: 'package.json',
|
|
639
|
-
content: JSON.stringify(pkg, null, 2),
|
|
640
|
-
skipIfExists: true,
|
|
641
|
-
integrateTarget: false,
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
function generateTsConfig(): GeneratedFile {
|
|
646
|
-
const config = {
|
|
647
|
-
compilerOptions: {
|
|
648
|
-
target: 'ES2020',
|
|
649
|
-
module: 'CommonJS',
|
|
650
|
-
lib: ['ES2020'],
|
|
651
|
-
declaration: true,
|
|
652
|
-
strict: true,
|
|
653
|
-
exactOptionalPropertyTypes: true,
|
|
654
|
-
esModuleInterop: true,
|
|
655
|
-
skipLibCheck: true,
|
|
656
|
-
forceConsistentCasingInFileNames: true,
|
|
657
|
-
resolveJsonModule: true,
|
|
658
|
-
outDir: './lib',
|
|
659
|
-
rootDir: './src',
|
|
660
|
-
},
|
|
661
|
-
include: ['src/**/*'],
|
|
662
|
-
exclude: ['node_modules', 'lib', '**/*.spec.ts'],
|
|
663
|
-
};
|
|
664
|
-
|
|
665
|
-
return {
|
|
666
|
-
path: 'tsconfig.json',
|
|
667
|
-
content: JSON.stringify(config, null, 2),
|
|
668
|
-
skipIfExists: true,
|
|
669
|
-
integrateTarget: false,
|
|
670
|
-
};
|
|
671
|
-
}
|
package/src/node/enums.ts
CHANGED
|
@@ -19,6 +19,15 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
19
19
|
// Check baseline surface for representation and values
|
|
20
20
|
const baselineEnum = ctx.apiSurface?.enums?.[enumDef.name];
|
|
21
21
|
const baselineAlias = ctx.apiSurface?.typeAliases?.[enumDef.name];
|
|
22
|
+
const generatedPath = `src/${dirName}/interfaces/${fileName(enumDef.name)}.interface.ts`;
|
|
23
|
+
|
|
24
|
+
// If the baseline already provides this enum from a different file (e.g., `.enum.ts`),
|
|
25
|
+
// skip generation to avoid duplicate exports from the same barrel.
|
|
26
|
+
const baselineSourceFile = (baselineEnum as any)?.sourceFile ?? (baselineAlias as any)?.sourceFile;
|
|
27
|
+
if (baselineSourceFile && baselineSourceFile !== generatedPath) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
const lines: string[] = [];
|
|
23
32
|
|
|
24
33
|
// Track whether the generated content has new values not in the baseline.
|