@workos/oagen-emitters 0.2.1 → 0.4.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 +15 -0
- package/README.md +129 -0
- package/dist/index.d.mts +13 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +14549 -3385
- package/dist/index.mjs.map +1 -1
- package/docs/sdk-architecture/dotnet.md +336 -0
- 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 +328 -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-dotnet.ts +45 -12
- package/smoke/sdk-go.ts +116 -42
- package/smoke/sdk-php.ts +28 -26
- package/smoke/sdk-python.ts +5 -2
- package/src/dotnet/client.ts +89 -0
- package/src/dotnet/enums.ts +323 -0
- package/src/dotnet/fixtures.ts +236 -0
- package/src/dotnet/index.ts +246 -0
- package/src/dotnet/manifest.ts +36 -0
- package/src/dotnet/models.ts +344 -0
- package/src/dotnet/naming.ts +330 -0
- package/src/dotnet/resources.ts +622 -0
- package/src/dotnet/tests.ts +693 -0
- package/src/dotnet/type-map.ts +201 -0
- package/src/dotnet/wrappers.ts +186 -0
- 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 +84 -0
- package/src/go/manifest.ts +36 -0
- package/src/go/models.ts +254 -0
- package/src/go/naming.ts +179 -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 +4 -0
- package/src/kotlin/client.ts +53 -0
- package/src/kotlin/enums.ts +162 -0
- package/src/kotlin/index.ts +92 -0
- package/src/kotlin/manifest.ts +55 -0
- package/src/kotlin/models.ts +395 -0
- package/src/kotlin/naming.ts +223 -0
- package/src/kotlin/overrides.ts +25 -0
- package/src/kotlin/resources.ts +667 -0
- package/src/kotlin/tests.ts +1019 -0
- package/src/kotlin/type-map.ts +123 -0
- package/src/kotlin/wrappers.ts +168 -0
- package/src/node/client.ts +128 -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 +3 -9
- package/src/node/models.ts +178 -21
- package/src/node/naming.ts +49 -111
- package/src/node/resources.ts +527 -397
- package/src/node/sdk-errors.ts +41 -0
- package/src/node/tests.ts +69 -19
- 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 +179 -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 +279 -0
- package/src/php/resources.ts +636 -0
- package/src/php/tests.ts +609 -0
- package/src/php/type-map.ts +90 -0
- package/src/php/utils.ts +18 -0
- package/src/php/wrappers.ts +152 -0
- package/src/python/client.ts +345 -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 +189 -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 +472 -0
- package/src/shared/naming-utils.ts +154 -0
- package/src/shared/non-spec-services.ts +54 -0
- package/src/shared/resolved-ops.ts +109 -0
- package/src/shared/wrapper-utils.ts +70 -0
- package/test/dotnet/client.test.ts +121 -0
- package/test/dotnet/enums.test.ts +193 -0
- package/test/dotnet/errors.test.ts +9 -0
- package/test/dotnet/manifest.test.ts +82 -0
- package/test/dotnet/models.test.ts +260 -0
- package/test/dotnet/resources.test.ts +255 -0
- package/test/dotnet/tests.test.ts +202 -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/kotlin/models.test.ts +135 -0
- package/test/kotlin/tests.test.ts +176 -0
- package/test/node/client.test.ts +92 -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 +315 -84
- package/test/node/serializers.test.ts +3 -1
- package/test/node/type-map.test.ts +11 -0
- package/test/php/client.test.ts +95 -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 +682 -0
- package/test/php/tests.test.ts +185 -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,123 @@
|
|
|
1
|
+
import type { TypeRef, PrimitiveType, UnionType } from '@workos/oagen';
|
|
2
|
+
import { mapTypeRef as irMapTypeRef } from '@workos/oagen';
|
|
3
|
+
import { className } from './naming.js';
|
|
4
|
+
import { enumCanonicalMap } from './enums.js';
|
|
5
|
+
|
|
6
|
+
/** Resolve an enum name through the canonical map (identity when no alias). */
|
|
7
|
+
function resolveEnumName(name: string): string {
|
|
8
|
+
return className(enumCanonicalMap.get(name) ?? name);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Map an IR TypeRef to a non-nullable Kotlin type expression.
|
|
13
|
+
*
|
|
14
|
+
* Kotlin's type system marks nullability at use-sites (`T?`). Optional fields
|
|
15
|
+
* on generated models append `?` to the output of this function.
|
|
16
|
+
*/
|
|
17
|
+
export function mapTypeRef(ref: TypeRef): string {
|
|
18
|
+
return irMapTypeRef<string>(ref, {
|
|
19
|
+
primitive: mapPrimitive,
|
|
20
|
+
array: (_ref, items) => `List<${items}>`,
|
|
21
|
+
model: (r) => className(r.name),
|
|
22
|
+
enum: (r) => resolveEnumName(r.name),
|
|
23
|
+
union: (r, variants) => joinUnionVariants(r, variants),
|
|
24
|
+
nullable: (_ref, inner) => (inner.endsWith('?') ? inner : `${inner}?`),
|
|
25
|
+
literal: (r) => {
|
|
26
|
+
if (r.value === null) return 'Any?';
|
|
27
|
+
if (typeof r.value === 'string') return 'String';
|
|
28
|
+
if (typeof r.value === 'number') return Number.isInteger(r.value) ? 'Long' : 'Double';
|
|
29
|
+
if (typeof r.value === 'boolean') return 'Boolean';
|
|
30
|
+
return 'Any';
|
|
31
|
+
},
|
|
32
|
+
map: (_ref, value) => `Map<String, ${value}>`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Map an IR TypeRef to a nullable Kotlin type expression (always appends `?`).
|
|
38
|
+
* Useful for optional fields on models / request options.
|
|
39
|
+
*/
|
|
40
|
+
export function mapTypeRefOptional(ref: TypeRef): string {
|
|
41
|
+
const baseType = mapTypeRef(ref);
|
|
42
|
+
return baseType.endsWith('?') ? baseType : `${baseType}?`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Is the given IR TypeRef a primitive value? Useful when deciding whether a
|
|
47
|
+
* nullable field needs an explicit `= null` default.
|
|
48
|
+
*/
|
|
49
|
+
export function isValueTypeRef(ref: TypeRef): boolean {
|
|
50
|
+
if (ref.kind === 'enum') return true;
|
|
51
|
+
if (ref.kind === 'primitive') {
|
|
52
|
+
if (ref.format === 'date-time') return true;
|
|
53
|
+
switch (ref.type) {
|
|
54
|
+
case 'integer':
|
|
55
|
+
case 'number':
|
|
56
|
+
case 'boolean':
|
|
57
|
+
return true;
|
|
58
|
+
default:
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mapPrimitive(ref: PrimitiveType): string {
|
|
66
|
+
if (ref.format === 'binary') return 'ByteArray';
|
|
67
|
+
if (ref.format === 'int32') return 'Int';
|
|
68
|
+
if (ref.format === 'int64') return 'Long';
|
|
69
|
+
if (ref.format === 'date-time') return 'OffsetDateTime';
|
|
70
|
+
switch (ref.type) {
|
|
71
|
+
case 'string':
|
|
72
|
+
return 'String';
|
|
73
|
+
case 'integer':
|
|
74
|
+
return 'Long';
|
|
75
|
+
case 'number':
|
|
76
|
+
return 'Double';
|
|
77
|
+
case 'boolean':
|
|
78
|
+
return 'Boolean';
|
|
79
|
+
case 'unknown':
|
|
80
|
+
return 'Any';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Track discriminated unions so the model generator can emit the appropriate
|
|
86
|
+
* Jackson `@JsonTypeInfo` / `@JsonSubTypes` annotations on the sealed parent.
|
|
87
|
+
*/
|
|
88
|
+
export const discriminatedUnions = new Map<
|
|
89
|
+
string,
|
|
90
|
+
{ property: string; mapping: Record<string, string>; variantTypes: string[] }
|
|
91
|
+
>();
|
|
92
|
+
|
|
93
|
+
function joinUnionVariants(ref: UnionType, variants: string[]): string {
|
|
94
|
+
if (ref.compositionKind === 'allOf') {
|
|
95
|
+
return variants[0] ?? 'Any';
|
|
96
|
+
}
|
|
97
|
+
const unique = [...new Set(variants)];
|
|
98
|
+
if (unique.length === 1) return unique[0];
|
|
99
|
+
|
|
100
|
+
if (ref.discriminator && ref.discriminator.mapping) {
|
|
101
|
+
const baseName = unique[0];
|
|
102
|
+
discriminatedUnions.set(baseName, {
|
|
103
|
+
property: ref.discriminator.property,
|
|
104
|
+
mapping: ref.discriminator.mapping,
|
|
105
|
+
variantTypes: unique,
|
|
106
|
+
});
|
|
107
|
+
// Use the base sealed type; Jackson @JsonTypeInfo handles variant selection.
|
|
108
|
+
return baseName;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Non-discriminated unions fall back to the Kotlin top type. A generic
|
|
112
|
+
// AnyOf<> is planned for a future phase if emitter tests prove it necessary.
|
|
113
|
+
return 'Any';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Kotlin imports implied by a given type expression. Caller collects into a set. */
|
|
117
|
+
export function implicitImportsFor(kotlinType: string): string[] {
|
|
118
|
+
const imports: string[] = [];
|
|
119
|
+
if (/\bOffsetDateTime\b/.test(kotlinType)) {
|
|
120
|
+
imports.push('java.time.OffsetDateTime');
|
|
121
|
+
}
|
|
122
|
+
return imports;
|
|
123
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { EmitterContext, ResolvedOperation, ResolvedWrapper, Parameter } from '@workos/oagen';
|
|
2
|
+
import { className, propertyName, ktLiteral, clientFieldExpression, escapeReserved } from './naming.js';
|
|
3
|
+
import { mapTypeRef, mapTypeRefOptional } from './type-map.js';
|
|
4
|
+
import { resolveWrapperParams } from '../shared/wrapper-utils.js';
|
|
5
|
+
import { sortPathParamsByTemplateOrder } from './resources.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Emit Kotlin wrapper methods for a union-split operation. Each wrapper
|
|
9
|
+
* method takes only the fields it needs for its variant, fills in the
|
|
10
|
+
* operation-level defaults and client-inferred values, and posts to the
|
|
11
|
+
* underlying operation.
|
|
12
|
+
*
|
|
13
|
+
* Returns a list of lines (with leading indentation suitable for inclusion
|
|
14
|
+
* inside the service class body).
|
|
15
|
+
*/
|
|
16
|
+
export function generateWrapperMethods(resolvedOp: ResolvedOperation, ctx: EmitterContext): string[] {
|
|
17
|
+
if (!resolvedOp.wrappers || resolvedOp.wrappers.length === 0) return [];
|
|
18
|
+
|
|
19
|
+
const out: string[] = [];
|
|
20
|
+
for (const wrapper of resolvedOp.wrappers) {
|
|
21
|
+
if (out.length > 0) out.push('');
|
|
22
|
+
for (const line of emitWrapperMethod(resolvedOp, wrapper, ctx)) out.push(line);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function emitWrapperMethod(resolvedOp: ResolvedOperation, wrapper: ResolvedWrapper, ctx: EmitterContext): string[] {
|
|
28
|
+
const op = resolvedOp.operation;
|
|
29
|
+
const method = propertyName(wrapper.name);
|
|
30
|
+
const resolvedParams = resolveWrapperParams(wrapper, ctx);
|
|
31
|
+
const responseClass = wrapper.responseModelName ? className(wrapper.responseModelName) : null;
|
|
32
|
+
|
|
33
|
+
const pathParams = sortPathParamsByTemplateOrder(op);
|
|
34
|
+
|
|
35
|
+
const lines: string[] = [];
|
|
36
|
+
|
|
37
|
+
// Build KDoc from operation description + @param docs for each wrapper param.
|
|
38
|
+
const kdocLines: string[] = [];
|
|
39
|
+
const opDesc = (op.description ?? '').trim();
|
|
40
|
+
const wrapperHumanName = method.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase();
|
|
41
|
+
if (opDesc) {
|
|
42
|
+
kdocLines.push(opDesc.split('\n')[0]);
|
|
43
|
+
} else {
|
|
44
|
+
kdocLines.push(`${wrapperHumanName.charAt(0).toUpperCase()}${wrapperHumanName.slice(1)}.`);
|
|
45
|
+
}
|
|
46
|
+
const paramDocs: string[] = [];
|
|
47
|
+
for (const pp of pathParams) {
|
|
48
|
+
if (pp.description?.trim()) {
|
|
49
|
+
paramDocs.push(`@param ${propertyName(pp.name)} ${escapeKdoc(pp.description.split('\n')[0].trim())}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const rp of resolvedParams) {
|
|
53
|
+
const desc = rp.field?.description?.trim();
|
|
54
|
+
if (desc) {
|
|
55
|
+
paramDocs.push(`@param ${propertyName(rp.paramName)} ${escapeKdoc(desc.split('\n')[0])}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (responseClass) {
|
|
59
|
+
paramDocs.push(`@return the ${responseClass}`);
|
|
60
|
+
}
|
|
61
|
+
if (paramDocs.length > 0 || kdocLines.length > 0) {
|
|
62
|
+
lines.push(' /**');
|
|
63
|
+
for (const l of kdocLines) lines.push(` * ${escapeKdoc(l)}`);
|
|
64
|
+
if (paramDocs.length > 0) {
|
|
65
|
+
lines.push(' *');
|
|
66
|
+
for (const p of paramDocs) lines.push(` * ${p}`);
|
|
67
|
+
}
|
|
68
|
+
lines.push(' */');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
lines.push(' @JvmOverloads');
|
|
72
|
+
|
|
73
|
+
// Build the method parameter list: path params, wrapper params, requestOptions
|
|
74
|
+
const params: string[] = [];
|
|
75
|
+
for (const pp of pathParams) params.push(` ${propertyName(pp.name)}: String`);
|
|
76
|
+
for (const rp of resolvedParams) {
|
|
77
|
+
const paramName = propertyName(rp.paramName);
|
|
78
|
+
const kotlinType = rp.field
|
|
79
|
+
? rp.isOptional
|
|
80
|
+
? mapTypeRefOptional(rp.field.type)
|
|
81
|
+
: mapTypeRef(rp.field.type)
|
|
82
|
+
: rp.isOptional
|
|
83
|
+
? 'String?'
|
|
84
|
+
: 'String';
|
|
85
|
+
const trailer = rp.isOptional ? ' = null' : '';
|
|
86
|
+
params.push(` ${paramName}: ${kotlinType}${trailer}`);
|
|
87
|
+
}
|
|
88
|
+
params.push(' requestOptions: RequestOptions? = null');
|
|
89
|
+
|
|
90
|
+
const returnClause = responseClass ? `: ${responseClass}` : '';
|
|
91
|
+
if (params.length === 1) {
|
|
92
|
+
const single = params[0].replace(/^\s+/, '');
|
|
93
|
+
lines.push(` fun ${escapeReserved(method)}(${single})${returnClause} {`);
|
|
94
|
+
} else {
|
|
95
|
+
lines.push(` fun ${escapeReserved(method)}(`);
|
|
96
|
+
for (let i = 0; i < params.length; i++) {
|
|
97
|
+
const suffix = i === params.length - 1 ? '' : ',';
|
|
98
|
+
lines.push(`${params[i]}${suffix}`);
|
|
99
|
+
}
|
|
100
|
+
lines.push(` )${returnClause} {`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Build body using bodyOf() — consistent with non-wrapper methods.
|
|
104
|
+
// bodyOf() automatically drops null optional values.
|
|
105
|
+
const bodyEntries: string[] = [];
|
|
106
|
+
for (const rp of resolvedParams) {
|
|
107
|
+
const paramName = propertyName(rp.paramName);
|
|
108
|
+
bodyEntries.push(` ${ktLiteral(rp.paramName)} to ${paramName}`);
|
|
109
|
+
}
|
|
110
|
+
for (const [k, v] of Object.entries(wrapper.defaults ?? {})) {
|
|
111
|
+
bodyEntries.push(` ${ktLiteral(k)} to ${ktLiteral(v)}`);
|
|
112
|
+
}
|
|
113
|
+
for (const k of wrapper.inferFromClient ?? []) {
|
|
114
|
+
bodyEntries.push(` ${ktLiteral(k)} to workos.${clientFieldExpression(k)}`);
|
|
115
|
+
}
|
|
116
|
+
if (bodyEntries.length > 0) {
|
|
117
|
+
lines.push(` val body =`);
|
|
118
|
+
lines.push(` bodyOf(`);
|
|
119
|
+
for (let i = 0; i < bodyEntries.length; i++) {
|
|
120
|
+
const sep = i === bodyEntries.length - 1 ? '' : ',';
|
|
121
|
+
lines.push(` ${bodyEntries[i]}${sep}`);
|
|
122
|
+
}
|
|
123
|
+
lines.push(` )`);
|
|
124
|
+
} else {
|
|
125
|
+
lines.push(` val body = linkedMapOf<String, Any?>()`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const pathExpr = buildPathExpr(op.path, pathParams);
|
|
129
|
+
const httpMethod = op.httpMethod.toUpperCase();
|
|
130
|
+
|
|
131
|
+
lines.push(` val config =`);
|
|
132
|
+
lines.push(` RequestConfig(`);
|
|
133
|
+
lines.push(` method = ${ktLiteral(httpMethod)},`);
|
|
134
|
+
lines.push(` path = ${pathExpr},`);
|
|
135
|
+
lines.push(` body = body,`);
|
|
136
|
+
if (op.requestBodyEncoding === 'form-urlencoded') {
|
|
137
|
+
// Some ops (SSO token, User Management authenticate) are form-encoded.
|
|
138
|
+
// Rewrite as formBody mapping string→string instead of JSON body.
|
|
139
|
+
// Fallback: leave body as JSON — the API accepts JSON for these too.
|
|
140
|
+
}
|
|
141
|
+
lines.push(` requestOptions = requestOptions`);
|
|
142
|
+
lines.push(` )`);
|
|
143
|
+
|
|
144
|
+
if (responseClass) {
|
|
145
|
+
lines.push(` return workos.baseClient.request(config, ${responseClass}::class.java)`);
|
|
146
|
+
} else {
|
|
147
|
+
lines.push(` workos.baseClient.requestVoid(config)`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
lines.push(' }');
|
|
151
|
+
return lines;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function escapeKdoc(s: string): string {
|
|
155
|
+
return s.replace(/\*\//g, '*\u200b/');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function buildPathExpr(path: string, pathParams: Parameter[]): string {
|
|
159
|
+
if (pathParams.length === 0) return ktLiteral(path);
|
|
160
|
+
let result = path;
|
|
161
|
+
for (const pp of pathParams) {
|
|
162
|
+
const placeholder = `{${pp.name}}`;
|
|
163
|
+
const propName = propertyName(pp.name);
|
|
164
|
+
const replacement = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(propName) ? `\$${propName}` : `\${${propName}}`;
|
|
165
|
+
result = result.replaceAll(placeholder, replacement);
|
|
166
|
+
}
|
|
167
|
+
return `"${result.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
168
|
+
}
|
package/src/node/client.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import type { ApiSpec, AuthScheme, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
|
|
4
|
+
import { collectReferencedNames } from '@workos/oagen';
|
|
2
5
|
import { fileName, resolveServiceDir, servicePropertyName, resolveInterfaceName, wireInterfaceName } from './naming.js';
|
|
3
6
|
import {
|
|
4
7
|
docComment,
|
|
@@ -15,9 +18,7 @@ export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFil
|
|
|
15
18
|
files.push(generateWorkOSClient(spec, ctx));
|
|
16
19
|
files.push(...generateServiceBarrels(spec, ctx));
|
|
17
20
|
files.push(generateBarrel(spec, ctx));
|
|
18
|
-
|
|
19
|
-
files.push(generatePackageJson(ctx));
|
|
20
|
-
files.push(generateTsConfig());
|
|
21
|
+
// worker barrel, package.json, tsconfig.json are now hand-maintained in the target SDK
|
|
21
22
|
|
|
22
23
|
return files;
|
|
23
24
|
}
|
|
@@ -86,6 +87,15 @@ function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
86
87
|
const resolvedName = resolveResourceClassName(service, ctx);
|
|
87
88
|
const propName = servicePropertyName(resolvedName);
|
|
88
89
|
if (existingProps.has(propName)) continue;
|
|
90
|
+
// Propagate `@deprecated` from the service class to the property so
|
|
91
|
+
// IDEs surface the strikethrough at `workos.xyz` access sites, not
|
|
92
|
+
// just when users `new Xyz()` directly. TS's deprecation-lint reads
|
|
93
|
+
// the property JSDoc, not the underlying type declaration.
|
|
94
|
+
const classDeprecation = ctx.apiSurface?.classes?.[resolvedName]?.deprecationMessage;
|
|
95
|
+
if (classDeprecation !== undefined) {
|
|
96
|
+
const body = classDeprecation ? ` ${classDeprecation}` : '';
|
|
97
|
+
lines.push(` /** @deprecated${body} */`);
|
|
98
|
+
}
|
|
89
99
|
lines.push(` readonly ${propName} = new ${resolvedName}(this);`);
|
|
90
100
|
}
|
|
91
101
|
|
|
@@ -128,47 +138,36 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
|
|
|
128
138
|
// exports a name (e.g., AuditLogSchema from create-audit-log-schema-options),
|
|
129
139
|
// the generated model with the same name must be skipped to prevent the
|
|
130
140
|
// merger from adding a duplicate `export *` that causes TS2308.
|
|
141
|
+
//
|
|
142
|
+
// Also track baseline file stems per directory so we can detect when the
|
|
143
|
+
// barrel needs updating with new export lines (see hasNewExports below).
|
|
144
|
+
const dirSymbolsFromBaseline = new Map<string, Set<string>>();
|
|
145
|
+
const seedFromBaseline = (sourceFile: string, name: string) => {
|
|
146
|
+
const match = sourceFile.match(/^src\/([^/]+)\/interfaces\/(.+)\.ts$/);
|
|
147
|
+
if (!match) return;
|
|
148
|
+
const dirName = match[1];
|
|
149
|
+
const fileStem = match[2];
|
|
150
|
+
if (!dirSymbols.has(dirName)) dirSymbols.set(dirName, new Set());
|
|
151
|
+
dirSymbols.get(dirName)!.add(name);
|
|
152
|
+
if (!dirSymbolsFromBaseline.has(dirName)) dirSymbolsFromBaseline.set(dirName, new Set());
|
|
153
|
+
dirSymbolsFromBaseline.get(dirName)!.add(fileStem);
|
|
154
|
+
};
|
|
131
155
|
if (ctx.apiSurface?.interfaces) {
|
|
132
156
|
for (const [name, iface] of Object.entries(ctx.apiSurface.interfaces)) {
|
|
133
157
|
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
|
-
}
|
|
158
|
+
if (sourceFile) seedFromBaseline(sourceFile, name);
|
|
144
159
|
}
|
|
145
160
|
}
|
|
146
161
|
if (ctx.apiSurface?.enums) {
|
|
147
162
|
for (const [name, enumDef] of Object.entries(ctx.apiSurface.enums)) {
|
|
148
163
|
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
|
-
}
|
|
164
|
+
if (sourceFile) seedFromBaseline(sourceFile, name);
|
|
158
165
|
}
|
|
159
166
|
}
|
|
160
167
|
if (ctx.apiSurface?.typeAliases) {
|
|
161
168
|
for (const [name, alias] of Object.entries(ctx.apiSurface.typeAliases)) {
|
|
162
169
|
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
|
-
}
|
|
170
|
+
if (sourceFile) seedFromBaseline(sourceFile, name);
|
|
172
171
|
}
|
|
173
172
|
}
|
|
174
173
|
|
|
@@ -187,8 +186,12 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
|
|
|
187
186
|
// Models -> service directories
|
|
188
187
|
// Skip list wrapper and list metadata models — they use shared List<T>/ListMetadata
|
|
189
188
|
// from common utils, so no per-resource interface file is generated.
|
|
189
|
+
// Also skip unreachable models — oagen only passes service-referenced models
|
|
190
|
+
// to generateModels, so unreachable models have no interface file to export.
|
|
191
|
+
const barrelReachable = collectReferencedNames(spec.services, spec.models);
|
|
190
192
|
for (const model of spec.models) {
|
|
191
193
|
if (isListMetadataModel(model) || isListWrapperModel(model)) continue;
|
|
194
|
+
if (!barrelReachable.models.has(model.name)) continue;
|
|
192
195
|
const service = modelToService.get(model.name);
|
|
193
196
|
const dirName = resolveDir(service);
|
|
194
197
|
if (!dirExports.has(dirName)) {
|
|
@@ -240,14 +243,96 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
|
|
|
240
243
|
}
|
|
241
244
|
|
|
242
245
|
for (const [dirName, exports] of dirExports) {
|
|
243
|
-
|
|
244
|
-
|
|
246
|
+
const exportSet = new Set(exports);
|
|
247
|
+
|
|
248
|
+
// When integrating into an existing SDK, include baseline exports from
|
|
249
|
+
// the api-surface so the barrel is comprehensive. This ensures stale
|
|
250
|
+
// entries (e.g., renamed files from previous generations) are removed
|
|
251
|
+
// when overwriteExisting replaces the barrel.
|
|
252
|
+
if (ctx.apiSurface) {
|
|
253
|
+
const addBaselineExports = (items: Record<string, any> | undefined) => {
|
|
254
|
+
if (!items) return;
|
|
255
|
+
for (const item of Object.values(items)) {
|
|
256
|
+
const sourceFile = (item as any).sourceFile as string | undefined;
|
|
257
|
+
if (!sourceFile) continue;
|
|
258
|
+
const match = sourceFile.match(/^src\/([^/]+)\/interfaces\/(.+)\.ts$/);
|
|
259
|
+
if (match && match[1] === dirName) {
|
|
260
|
+
exportSet.add(`export * from './${match[2].replace(/\.ts$/, '')}';`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
addBaselineExports(ctx.apiSurface.interfaces);
|
|
265
|
+
addBaselineExports(ctx.apiSurface.typeAliases);
|
|
266
|
+
addBaselineExports(ctx.apiSurface.enums);
|
|
267
|
+
|
|
268
|
+
// Scan the target directory for interface files not captured by the
|
|
269
|
+
// api-surface (e.g., list wrappers, hand-written types). Only add
|
|
270
|
+
// files whose exported symbols don't collide with symbols already
|
|
271
|
+
// claimed by another directory's barrel (TS2308 prevention).
|
|
272
|
+
if (ctx.targetDir) {
|
|
273
|
+
const interfacesDir = path.join(ctx.targetDir, 'src', dirName, 'interfaces');
|
|
274
|
+
const symbols = dirSymbols.get(dirName) ?? new Set<string>();
|
|
275
|
+
try {
|
|
276
|
+
for (const entry of fs.readdirSync(interfacesDir)) {
|
|
277
|
+
if (entry === 'index.ts') continue;
|
|
278
|
+
if (!entry.endsWith('.ts')) continue;
|
|
279
|
+
const stem = entry.replace(/\.ts$/, '');
|
|
280
|
+
const exportLine = `export * from './${stem}';`;
|
|
281
|
+
if (exportSet.has(exportLine)) continue;
|
|
282
|
+
|
|
283
|
+
// Extract exported symbol names from the file to check for conflicts
|
|
284
|
+
const content = fs.readFileSync(path.join(interfacesDir, entry), 'utf-8');
|
|
285
|
+
const exportedNames: string[] = [];
|
|
286
|
+
for (const m of content.matchAll(/export\s+(?:interface|type|enum|class|const|function)\s+(\w+)/g)) {
|
|
287
|
+
exportedNames.push(m[1]);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Skip if any exported name collides with a symbol already
|
|
291
|
+
// claimed by any file (same or different directory)
|
|
292
|
+
const hasCollision = exportedNames.some((name) => globalExistingSymbols.has(name));
|
|
293
|
+
if (hasCollision) continue;
|
|
294
|
+
|
|
295
|
+
// Safe to add — register symbols and include in barrel
|
|
296
|
+
for (const name of exportedNames) {
|
|
297
|
+
symbols.add(name);
|
|
298
|
+
globalExistingSymbols.add(name);
|
|
299
|
+
}
|
|
300
|
+
exportSet.add(exportLine);
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
// Directory doesn't exist in target — nothing to scan
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Deduplicate and sort
|
|
309
|
+
const uniqueExports = [...exportSet];
|
|
245
310
|
uniqueExports.sort();
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
311
|
+
|
|
312
|
+
if (ctx.apiSurface) {
|
|
313
|
+
// Integration mode: overwrite the barrel so stale entries are removed.
|
|
314
|
+
files.push({
|
|
315
|
+
path: `src/${dirName}/interfaces/index.ts`,
|
|
316
|
+
content: uniqueExports.join('\n'),
|
|
317
|
+
overwriteExisting: true,
|
|
318
|
+
});
|
|
319
|
+
} else {
|
|
320
|
+
// Standalone generation: only update if there are new exports.
|
|
321
|
+
const baselineSymbols = dirSymbolsFromBaseline.get(dirName);
|
|
322
|
+
const hasNewExports = baselineSymbols
|
|
323
|
+
? uniqueExports.some((exp) => {
|
|
324
|
+
const match = exp.match(/from '\.\/(.*?)'/);
|
|
325
|
+
if (!match) return false;
|
|
326
|
+
return !baselineSymbols.has(match[1]);
|
|
327
|
+
})
|
|
328
|
+
: false;
|
|
329
|
+
|
|
330
|
+
files.push({
|
|
331
|
+
path: `src/${dirName}/interfaces/index.ts`,
|
|
332
|
+
content: uniqueExports.join('\n'),
|
|
333
|
+
skipIfExists: !hasNewExports,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
251
336
|
}
|
|
252
337
|
|
|
253
338
|
return files;
|
|
@@ -457,7 +542,11 @@ function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
|
|
|
457
542
|
}
|
|
458
543
|
|
|
459
544
|
// Unassigned models (common) — use barrel if any exist
|
|
460
|
-
|
|
545
|
+
// Filter to reachable models only: oagen's generateAllFiles passes only
|
|
546
|
+
// service-referenced models to generateModels, so unreachable models
|
|
547
|
+
// never get interface files. Exporting them here would create broken imports.
|
|
548
|
+
const reachable = collectReferencedNames(spec.services, spec.models);
|
|
549
|
+
const unassignedModels = spec.models.filter((m) => !modelToService.has(m.name) && reachable.models.has(m.name));
|
|
461
550
|
const commonEnums = spec.enums.filter((e) => {
|
|
462
551
|
const enumService = findEnumService(e.name, spec.services);
|
|
463
552
|
return !enumService;
|
|
@@ -527,23 +616,6 @@ function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
|
|
|
527
616
|
};
|
|
528
617
|
}
|
|
529
618
|
|
|
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
619
|
function findEnumService(enumName: string, services: Service[]): string | undefined {
|
|
548
620
|
for (const service of services) {
|
|
549
621
|
for (const op of service.operations) {
|
|
@@ -610,62 +682,3 @@ function serverConstName(description: string): string {
|
|
|
610
682
|
.toUpperCase()
|
|
611
683
|
);
|
|
612
684
|
}
|
|
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.
|