workos 0.11.2 → 0.12.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/README.md +165 -6
- package/dist/bin.js +22 -1
- package/dist/bin.js.map +1 -1
- package/dist/check-coverage.ts +237 -0
- package/dist/commands/debug.js +0 -1
- package/dist/commands/debug.js.map +1 -1
- package/dist/commands/dev.d.ts +23 -0
- package/dist/commands/dev.js +139 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/emulate.d.ts +6 -0
- package/dist/commands/emulate.js +64 -0
- package/dist/commands/emulate.js.map +1 -0
- package/dist/commands/login.js +0 -4
- package/dist/commands/login.js.map +1 -1
- package/dist/emulate/core/id.d.ts +48 -0
- package/dist/emulate/core/id.js +73 -0
- package/dist/emulate/core/id.js.map +1 -0
- package/dist/emulate/core/index.d.ts +8 -0
- package/dist/emulate/core/index.js +8 -0
- package/dist/emulate/core/index.js.map +1 -0
- package/dist/emulate/core/jwt.d.ts +28 -0
- package/dist/emulate/core/jwt.js +78 -0
- package/dist/emulate/core/jwt.js.map +1 -0
- package/dist/emulate/core/middleware/auth.d.ts +15 -0
- package/dist/emulate/core/middleware/auth.js +17 -0
- package/dist/emulate/core/middleware/auth.js.map +1 -0
- package/dist/emulate/core/middleware/error-handler.d.ts +22 -0
- package/dist/emulate/core/middleware/error-handler.js +72 -0
- package/dist/emulate/core/middleware/error-handler.js.map +1 -0
- package/dist/emulate/core/pagination.d.ts +27 -0
- package/dist/emulate/core/pagination.js +43 -0
- package/dist/emulate/core/pagination.js.map +1 -0
- package/dist/emulate/core/plugin.d.ts +15 -0
- package/dist/emulate/core/plugin.js +2 -0
- package/dist/emulate/core/plugin.js.map +1 -0
- package/dist/emulate/core/server.d.ts +17 -0
- package/dist/emulate/core/server.js +90 -0
- package/dist/emulate/core/server.js.map +1 -0
- package/dist/emulate/core/store.d.ts +44 -0
- package/dist/emulate/core/store.js +169 -0
- package/dist/emulate/core/store.js.map +1 -0
- package/dist/emulate/index.d.ts +25 -0
- package/dist/emulate/index.js +47 -0
- package/dist/emulate/index.js.map +1 -0
- package/dist/emulate/workos/constants.d.ts +56 -0
- package/dist/emulate/workos/constants.js +56 -0
- package/dist/emulate/workos/constants.js.map +1 -0
- package/dist/emulate/workos/entities.d.ts +360 -0
- package/dist/emulate/workos/entities.js +2 -0
- package/dist/emulate/workos/entities.js.map +1 -0
- package/dist/emulate/workos/event-bus.d.ts +17 -0
- package/dist/emulate/workos/event-bus.js +70 -0
- package/dist/emulate/workos/event-bus.js.map +1 -0
- package/dist/emulate/workos/helpers.d.ts +72 -0
- package/dist/emulate/workos/helpers.js +211 -0
- package/dist/emulate/workos/helpers.js.map +1 -0
- package/dist/emulate/workos/index.d.ts +91 -0
- package/dist/emulate/workos/index.js +322 -0
- package/dist/emulate/workos/index.js.map +1 -0
- package/dist/emulate/workos/role-helpers.d.ts +21 -0
- package/dist/emulate/workos/role-helpers.js +130 -0
- package/dist/emulate/workos/role-helpers.js.map +1 -0
- package/dist/emulate/workos/routes/api-keys.d.ts +2 -0
- package/dist/emulate/workos/routes/api-keys.js +32 -0
- package/dist/emulate/workos/routes/api-keys.js.map +1 -0
- package/dist/emulate/workos/routes/audit-logs.d.ts +2 -0
- package/dist/emulate/workos/routes/audit-logs.js +104 -0
- package/dist/emulate/workos/routes/audit-logs.js.map +1 -0
- package/dist/emulate/workos/routes/auth-challenges.d.ts +2 -0
- package/dist/emulate/workos/routes/auth-challenges.js +51 -0
- package/dist/emulate/workos/routes/auth-challenges.js.map +1 -0
- package/dist/emulate/workos/routes/auth-factors.d.ts +2 -0
- package/dist/emulate/workos/routes/auth-factors.js +51 -0
- package/dist/emulate/workos/routes/auth-factors.js.map +1 -0
- package/dist/emulate/workos/routes/auth.d.ts +2 -0
- package/dist/emulate/workos/routes/auth.js +350 -0
- package/dist/emulate/workos/routes/auth.js.map +1 -0
- package/dist/emulate/workos/routes/authorization-checks.d.ts +10 -0
- package/dist/emulate/workos/routes/authorization-checks.js +123 -0
- package/dist/emulate/workos/routes/authorization-checks.js.map +1 -0
- package/dist/emulate/workos/routes/authorization-org-roles.d.ts +2 -0
- package/dist/emulate/workos/routes/authorization-org-roles.js +64 -0
- package/dist/emulate/workos/routes/authorization-org-roles.js.map +1 -0
- package/dist/emulate/workos/routes/authorization-permissions.d.ts +2 -0
- package/dist/emulate/workos/routes/authorization-permissions.js +67 -0
- package/dist/emulate/workos/routes/authorization-permissions.js.map +1 -0
- package/dist/emulate/workos/routes/authorization-resources.d.ts +2 -0
- package/dist/emulate/workos/routes/authorization-resources.js +117 -0
- package/dist/emulate/workos/routes/authorization-resources.js.map +1 -0
- package/dist/emulate/workos/routes/authorization-roles.d.ts +2 -0
- package/dist/emulate/workos/routes/authorization-roles.js +13 -0
- package/dist/emulate/workos/routes/authorization-roles.js.map +1 -0
- package/dist/emulate/workos/routes/config.d.ts +2 -0
- package/dist/emulate/workos/routes/config.js +57 -0
- package/dist/emulate/workos/routes/config.js.map +1 -0
- package/dist/emulate/workos/routes/connect.d.ts +2 -0
- package/dist/emulate/workos/routes/connect.js +65 -0
- package/dist/emulate/workos/routes/connect.js.map +1 -0
- package/dist/emulate/workos/routes/connections.d.ts +2 -0
- package/dist/emulate/workos/routes/connections.js +73 -0
- package/dist/emulate/workos/routes/connections.js.map +1 -0
- package/dist/emulate/workos/routes/data-integrations.d.ts +2 -0
- package/dist/emulate/workos/routes/data-integrations.js +55 -0
- package/dist/emulate/workos/routes/data-integrations.js.map +1 -0
- package/dist/emulate/workos/routes/directories.d.ts +2 -0
- package/dist/emulate/workos/routes/directories.js +90 -0
- package/dist/emulate/workos/routes/directories.js.map +1 -0
- package/dist/emulate/workos/routes/email-verification.d.ts +2 -0
- package/dist/emulate/workos/routes/email-verification.js +49 -0
- package/dist/emulate/workos/routes/email-verification.js.map +1 -0
- package/dist/emulate/workos/routes/events.d.ts +2 -0
- package/dist/emulate/workos/routes/events.js +18 -0
- package/dist/emulate/workos/routes/events.js.map +1 -0
- package/dist/emulate/workos/routes/feature-flags.d.ts +2 -0
- package/dist/emulate/workos/routes/feature-flags.js +103 -0
- package/dist/emulate/workos/routes/feature-flags.js.map +1 -0
- package/dist/emulate/workos/routes/invitations.d.ts +2 -0
- package/dist/emulate/workos/routes/invitations.js +122 -0
- package/dist/emulate/workos/routes/invitations.js.map +1 -0
- package/dist/emulate/workos/routes/legacy-mfa.d.ts +2 -0
- package/dist/emulate/workos/routes/legacy-mfa.js +75 -0
- package/dist/emulate/workos/routes/legacy-mfa.js.map +1 -0
- package/dist/emulate/workos/routes/magic-auth.d.ts +2 -0
- package/dist/emulate/workos/routes/magic-auth.js +32 -0
- package/dist/emulate/workos/routes/magic-auth.js.map +1 -0
- package/dist/emulate/workos/routes/memberships.d.ts +2 -0
- package/dist/emulate/workos/routes/memberships.js +114 -0
- package/dist/emulate/workos/routes/memberships.js.map +1 -0
- package/dist/emulate/workos/routes/organization-domains.d.ts +2 -0
- package/dist/emulate/workos/routes/organization-domains.js +58 -0
- package/dist/emulate/workos/routes/organization-domains.js.map +1 -0
- package/dist/emulate/workos/routes/organizations.d.ts +2 -0
- package/dist/emulate/workos/routes/organizations.js +131 -0
- package/dist/emulate/workos/routes/organizations.js.map +1 -0
- package/dist/emulate/workos/routes/password-reset.d.ts +2 -0
- package/dist/emulate/workos/routes/password-reset.js +61 -0
- package/dist/emulate/workos/routes/password-reset.js.map +1 -0
- package/dist/emulate/workos/routes/pipes.d.ts +2 -0
- package/dist/emulate/workos/routes/pipes.js +82 -0
- package/dist/emulate/workos/routes/pipes.js.map +1 -0
- package/dist/emulate/workos/routes/portal.d.ts +2 -0
- package/dist/emulate/workos/routes/portal.js +18 -0
- package/dist/emulate/workos/routes/portal.js.map +1 -0
- package/dist/emulate/workos/routes/radar.d.ts +2 -0
- package/dist/emulate/workos/routes/radar.js +41 -0
- package/dist/emulate/workos/routes/radar.js.map +1 -0
- package/dist/emulate/workos/routes/sessions.d.ts +2 -0
- package/dist/emulate/workos/routes/sessions.js +51 -0
- package/dist/emulate/workos/routes/sessions.js.map +1 -0
- package/dist/emulate/workos/routes/sso.d.ts +2 -0
- package/dist/emulate/workos/routes/sso.js +161 -0
- package/dist/emulate/workos/routes/sso.js.map +1 -0
- package/dist/emulate/workos/routes/user-features.d.ts +2 -0
- package/dist/emulate/workos/routes/user-features.js +50 -0
- package/dist/emulate/workos/routes/user-features.js.map +1 -0
- package/dist/emulate/workos/routes/users.d.ts +2 -0
- package/dist/emulate/workos/routes/users.js +129 -0
- package/dist/emulate/workos/routes/users.js.map +1 -0
- package/dist/emulate/workos/routes/webhook-endpoints.d.ts +2 -0
- package/dist/emulate/workos/routes/webhook-endpoints.js +66 -0
- package/dist/emulate/workos/routes/webhook-endpoints.js.map +1 -0
- package/dist/emulate/workos/routes/widgets.d.ts +2 -0
- package/dist/emulate/workos/routes/widgets.js +27 -0
- package/dist/emulate/workos/routes/widgets.js.map +1 -0
- package/dist/emulate/workos/store.d.ts +48 -0
- package/dist/emulate/workos/store.js +102 -0
- package/dist/emulate/workos/store.js.map +1 -0
- package/dist/emulate/workos/webhook-signer.d.ts +1 -0
- package/dist/emulate/workos/webhook-signer.js +8 -0
- package/dist/emulate/workos/webhook-signer.js.map +1 -0
- package/dist/gen-routes-lib.spec.ts +659 -0
- package/dist/gen-routes-lib.ts +647 -0
- package/dist/gen-routes.ts +96 -0
- package/dist/lib/dev-command.d.ts +26 -0
- package/dist/lib/dev-command.js +122 -0
- package/dist/lib/dev-command.js.map +1 -0
- package/dist/lib/run-with-core.js +0 -3
- package/dist/lib/run-with-core.js.map +1 -1
- package/dist/lib/settings.js +1 -1
- package/dist/lib/settings.js.map +1 -1
- package/dist/utils/help-json.js +1 -0
- package/dist/utils/help-json.js.map +1 -1
- package/dist/utils/register-subcommand.d.ts +5 -2
- package/dist/utils/register-subcommand.js +16 -19
- package/dist/utils/register-subcommand.js.map +1 -1
- package/package.json +21 -8
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core codegen logic for gen-routes. Separated from the CLI entry point
|
|
3
|
+
* so the transformation functions can be unit-tested independently.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// OpenAPI types (minimal subset we need)
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export interface OpenAPISpec {
|
|
11
|
+
openapi?: string;
|
|
12
|
+
info?: { title?: string; version?: string };
|
|
13
|
+
paths?: Record<string, PathItem>;
|
|
14
|
+
components?: { schemas?: Record<string, SchemaObject> };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PathItem {
|
|
18
|
+
get?: OperationObject;
|
|
19
|
+
post?: OperationObject;
|
|
20
|
+
put?: OperationObject;
|
|
21
|
+
patch?: OperationObject;
|
|
22
|
+
delete?: OperationObject;
|
|
23
|
+
parameters?: ParameterObject[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface OperationObject {
|
|
27
|
+
operationId?: string;
|
|
28
|
+
summary?: string;
|
|
29
|
+
tags?: string[];
|
|
30
|
+
parameters?: ParameterObject[];
|
|
31
|
+
requestBody?: {
|
|
32
|
+
content?: Record<string, { schema?: SchemaObject }>;
|
|
33
|
+
};
|
|
34
|
+
responses?: Record<
|
|
35
|
+
string,
|
|
36
|
+
{
|
|
37
|
+
description?: string;
|
|
38
|
+
content?: Record<string, { schema?: SchemaObject }>;
|
|
39
|
+
}
|
|
40
|
+
>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ParameterObject {
|
|
44
|
+
name: string;
|
|
45
|
+
in: 'path' | 'query' | 'header';
|
|
46
|
+
required?: boolean;
|
|
47
|
+
schema?: SchemaObject;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SchemaObject {
|
|
51
|
+
type?: string;
|
|
52
|
+
format?: string;
|
|
53
|
+
enum?: string[];
|
|
54
|
+
properties?: Record<string, SchemaObject>;
|
|
55
|
+
required?: string[];
|
|
56
|
+
items?: SchemaObject;
|
|
57
|
+
$ref?: string;
|
|
58
|
+
allOf?: SchemaObject[];
|
|
59
|
+
oneOf?: SchemaObject[];
|
|
60
|
+
anyOf?: SchemaObject[];
|
|
61
|
+
nullable?: boolean;
|
|
62
|
+
description?: string;
|
|
63
|
+
additionalProperties?: boolean | SchemaObject;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Parsed intermediate representation
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
export interface ParsedEntity {
|
|
71
|
+
/** PascalCase name, e.g. "Organization" */
|
|
72
|
+
name: string;
|
|
73
|
+
/** snake_case object type, e.g. "organization" */
|
|
74
|
+
objectType: string;
|
|
75
|
+
/** ID prefix, e.g. "org" */
|
|
76
|
+
idPrefix: string;
|
|
77
|
+
/** Fields beyond the base Entity (id, created_at, updated_at) */
|
|
78
|
+
fields: ParsedField[];
|
|
79
|
+
/** Fields to index in the store collection */
|
|
80
|
+
indexFields: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ParsedField {
|
|
84
|
+
name: string;
|
|
85
|
+
tsType: string;
|
|
86
|
+
nullable: boolean;
|
|
87
|
+
description?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ParsedRoute {
|
|
91
|
+
/** The resource tag, e.g. "organizations" */
|
|
92
|
+
tag: string;
|
|
93
|
+
/** Output filename, e.g. "organizations.ts" */
|
|
94
|
+
filename: string;
|
|
95
|
+
/** Function name, e.g. "organizationRoutes" */
|
|
96
|
+
functionName: string;
|
|
97
|
+
/** The collection accessor on WorkOSStore, e.g. "organizations" */
|
|
98
|
+
storeAccessor: string;
|
|
99
|
+
/** The formatter function name, e.g. "formatOrganization" */
|
|
100
|
+
formatterName: string;
|
|
101
|
+
/** Individual route operations */
|
|
102
|
+
operations: ParsedOperation[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ParsedOperation {
|
|
106
|
+
method: 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
107
|
+
path: string;
|
|
108
|
+
operationId?: string;
|
|
109
|
+
summary?: string;
|
|
110
|
+
/** Whether the path has an :id param */
|
|
111
|
+
hasIdParam: boolean;
|
|
112
|
+
/** Whether this is a list endpoint (GET without :id in the resource path) */
|
|
113
|
+
isList: boolean;
|
|
114
|
+
/** Query parameter names */
|
|
115
|
+
queryParams: string[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface ParsedSpec {
|
|
119
|
+
entities: ParsedEntity[];
|
|
120
|
+
routes: ParsedRoute[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface GeneratedOutput {
|
|
124
|
+
[filename: string]: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Spec parsing
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
/** Well-known ID prefixes matching src/emulate/core/id.ts */
|
|
132
|
+
const KNOWN_PREFIXES: Record<string, string> = {
|
|
133
|
+
organization: 'org',
|
|
134
|
+
organization_domain: 'org_domain',
|
|
135
|
+
organization_membership: 'om',
|
|
136
|
+
user: 'user',
|
|
137
|
+
session: 'session',
|
|
138
|
+
email_verification: 'email_verification',
|
|
139
|
+
password_reset: 'password_reset',
|
|
140
|
+
magic_auth: 'magic_auth',
|
|
141
|
+
authentication_factor: 'auth_factor',
|
|
142
|
+
authorization_code: 'auth_code',
|
|
143
|
+
identity: 'identity',
|
|
144
|
+
connection: 'conn',
|
|
145
|
+
connection_domain: 'conn_domain',
|
|
146
|
+
profile: 'prof',
|
|
147
|
+
sso_profile: 'prof',
|
|
148
|
+
sso_authorization: 'sso_auth',
|
|
149
|
+
directory: 'directory',
|
|
150
|
+
directory_user: 'directory_user',
|
|
151
|
+
directory_group: 'directory_grp',
|
|
152
|
+
event: 'event',
|
|
153
|
+
invitation: 'inv',
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** Base entity fields that are auto-managed — excluded from generated fields. */
|
|
157
|
+
const BASE_FIELDS = new Set(['id', 'created_at', 'updated_at']);
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Resolve a $ref to a schema name. Only handles local refs like
|
|
161
|
+
* "#/components/schemas/Organization".
|
|
162
|
+
*/
|
|
163
|
+
function resolveRefName(ref: string): string {
|
|
164
|
+
const parts = ref.split('/');
|
|
165
|
+
return parts[parts.length - 1];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function resolveSchema(schema: SchemaObject, spec: OpenAPISpec): SchemaObject {
|
|
169
|
+
if (schema.$ref) {
|
|
170
|
+
const name = resolveRefName(schema.$ref);
|
|
171
|
+
const resolved = spec.components?.schemas?.[name];
|
|
172
|
+
return resolved ? resolveSchema(resolved, spec) : schema;
|
|
173
|
+
}
|
|
174
|
+
if (schema.allOf) {
|
|
175
|
+
const merged: SchemaObject = { type: 'object', properties: {}, required: [] };
|
|
176
|
+
for (const sub of schema.allOf) {
|
|
177
|
+
const resolved = resolveSchema(sub, spec);
|
|
178
|
+
if (resolved.properties) {
|
|
179
|
+
Object.assign(merged.properties!, resolved.properties);
|
|
180
|
+
}
|
|
181
|
+
if (resolved.required) {
|
|
182
|
+
merged.required!.push(...resolved.required);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return merged;
|
|
186
|
+
}
|
|
187
|
+
return schema;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Convert an OpenAPI type + format to a TypeScript type string. */
|
|
191
|
+
export function schemaToTsType(schema: SchemaObject, spec: OpenAPISpec): string {
|
|
192
|
+
if (schema.$ref) {
|
|
193
|
+
const name = resolveRefName(schema.$ref);
|
|
194
|
+
const resolved = spec.components?.schemas?.[name];
|
|
195
|
+
if (resolved) return schemaToTsType(resolved, spec);
|
|
196
|
+
return 'unknown';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (schema.allOf) {
|
|
200
|
+
const resolved = resolveSchema(schema, spec);
|
|
201
|
+
return schemaToTsType(resolved, spec);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (schema.oneOf || schema.anyOf) {
|
|
205
|
+
const variants = (schema.oneOf ?? schema.anyOf)!;
|
|
206
|
+
const types = variants.map((v) => schemaToTsType(v, spec));
|
|
207
|
+
return types.join(' | ');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (schema.enum) {
|
|
211
|
+
return schema.enum.map((v) => `'${v}'`).join(' | ');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
switch (schema.type) {
|
|
215
|
+
case 'string':
|
|
216
|
+
return 'string';
|
|
217
|
+
case 'integer':
|
|
218
|
+
case 'number':
|
|
219
|
+
return 'number';
|
|
220
|
+
case 'boolean':
|
|
221
|
+
return 'boolean';
|
|
222
|
+
case 'array':
|
|
223
|
+
if (schema.items) {
|
|
224
|
+
return `${schemaToTsType(schema.items, spec)}[]`;
|
|
225
|
+
}
|
|
226
|
+
return 'unknown[]';
|
|
227
|
+
case 'object':
|
|
228
|
+
if (schema.additionalProperties) {
|
|
229
|
+
if (typeof schema.additionalProperties === 'boolean') {
|
|
230
|
+
return 'Record<string, unknown>';
|
|
231
|
+
}
|
|
232
|
+
const valType = schemaToTsType(schema.additionalProperties, spec);
|
|
233
|
+
return `Record<string, ${valType}>`;
|
|
234
|
+
}
|
|
235
|
+
if (schema.properties) {
|
|
236
|
+
const entries = Object.entries(schema.properties).map(([k, v]) => {
|
|
237
|
+
const t = schemaToTsType(v, spec);
|
|
238
|
+
return `${k}: ${t}`;
|
|
239
|
+
});
|
|
240
|
+
return `{ ${entries.join('; ')} }`;
|
|
241
|
+
}
|
|
242
|
+
return 'Record<string, unknown>';
|
|
243
|
+
default:
|
|
244
|
+
return 'unknown';
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Convert a schema name to snake_case. */
|
|
249
|
+
export function toSnakeCase(name: string): string {
|
|
250
|
+
return name
|
|
251
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
252
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
253
|
+
.toLowerCase();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Convert a snake_case string to PascalCase. */
|
|
257
|
+
export function toPascalCase(name: string): string {
|
|
258
|
+
return name
|
|
259
|
+
.split(/[_\-\s]+/)
|
|
260
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
261
|
+
.join('');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Convert a snake_case string to camelCase. */
|
|
265
|
+
export function toCamelCase(name: string): string {
|
|
266
|
+
const pascal = toPascalCase(name);
|
|
267
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Pluralize a simple English word (naive). */
|
|
271
|
+
export function pluralize(word: string): string {
|
|
272
|
+
if (word.endsWith('s') || word.endsWith('x') || word.endsWith('z')) return word + 'es';
|
|
273
|
+
if (word.endsWith('y') && !/[aeiou]y$/i.test(word)) return word.slice(0, -1) + 'ies';
|
|
274
|
+
return word + 's';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Singularize a simple English word (naive). */
|
|
278
|
+
export function singularize(word: string): string {
|
|
279
|
+
if (word.endsWith('ies')) return word.slice(0, -3) + 'y';
|
|
280
|
+
if (word.endsWith('ses') || word.endsWith('xes') || word.endsWith('zes')) return word.slice(0, -2);
|
|
281
|
+
if (word.endsWith('s') && !word.endsWith('ss')) return word.slice(0, -1);
|
|
282
|
+
return word;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Heuristic: guess which fields should be indexed for a collection.
|
|
287
|
+
* Looks at field names that end with _id or are common lookup fields.
|
|
288
|
+
*/
|
|
289
|
+
function guessIndexFields(fields: ParsedField[]): string[] {
|
|
290
|
+
const indexes: string[] = [];
|
|
291
|
+
for (const f of fields) {
|
|
292
|
+
if (f.name === 'object') continue;
|
|
293
|
+
if (f.name.endsWith('_id') && f.name !== 'external_id' && f.name !== 'stripe_customer_id' && f.name !== 'idp_id') {
|
|
294
|
+
indexes.push(f.name);
|
|
295
|
+
}
|
|
296
|
+
if (f.name === 'email' || f.name === 'code' || f.name === 'domain') {
|
|
297
|
+
indexes.push(f.name);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Also add external_id if present — the hand-written code indexes it for some collections
|
|
301
|
+
if (fields.some((f) => f.name === 'external_id')) {
|
|
302
|
+
indexes.push('external_id');
|
|
303
|
+
}
|
|
304
|
+
return indexes;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function extractEntityFromSchema(schemaName: string, schema: SchemaObject, spec: OpenAPISpec): ParsedEntity | null {
|
|
308
|
+
const resolved = resolveSchema(schema, spec);
|
|
309
|
+
if (resolved.type !== 'object' || !resolved.properties) return null;
|
|
310
|
+
|
|
311
|
+
const objectType = toSnakeCase(schemaName);
|
|
312
|
+
const required = new Set(resolved.required ?? []);
|
|
313
|
+
|
|
314
|
+
const fields: ParsedField[] = [];
|
|
315
|
+
for (const [propName, propSchema] of Object.entries(resolved.properties)) {
|
|
316
|
+
if (BASE_FIELDS.has(propName)) continue;
|
|
317
|
+
|
|
318
|
+
const resolvedProp = propSchema.$ref ? resolveSchema(propSchema, spec) : propSchema;
|
|
319
|
+
const tsType = schemaToTsType(resolvedProp, spec);
|
|
320
|
+
const nullable = resolvedProp.nullable === true || !required.has(propName);
|
|
321
|
+
|
|
322
|
+
fields.push({
|
|
323
|
+
name: propName,
|
|
324
|
+
tsType,
|
|
325
|
+
nullable,
|
|
326
|
+
description: resolvedProp.description,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (fields.length === 0) return null;
|
|
331
|
+
|
|
332
|
+
const idPrefix = KNOWN_PREFIXES[objectType] ?? objectType.replace(/_/g, '_').slice(0, 10);
|
|
333
|
+
const indexFields = guessIndexFields(fields);
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
name: schemaName,
|
|
337
|
+
objectType,
|
|
338
|
+
idPrefix,
|
|
339
|
+
fields,
|
|
340
|
+
indexFields,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Convert OpenAPI path "/organizations/{id}" to Hono path "/organizations/:id". */
|
|
345
|
+
export function openApiPathToHono(path: string): string {
|
|
346
|
+
return path.replace(/\{([^}]+)\}/g, ':$1');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function extractRoutes(spec: OpenAPISpec): Map<string, ParsedOperation[]> {
|
|
350
|
+
const tagOps = new Map<string, ParsedOperation[]>();
|
|
351
|
+
|
|
352
|
+
for (const [path, item] of Object.entries(spec.paths ?? {})) {
|
|
353
|
+
const methods: Array<'get' | 'post' | 'put' | 'patch' | 'delete'> = ['get', 'post', 'put', 'patch', 'delete'];
|
|
354
|
+
|
|
355
|
+
for (const method of methods) {
|
|
356
|
+
const op = item[method];
|
|
357
|
+
if (!op) continue;
|
|
358
|
+
|
|
359
|
+
const tag = op.tags?.[0] ?? inferTagFromPath(path);
|
|
360
|
+
const honoPath = openApiPathToHono(path);
|
|
361
|
+
const hasIdParam = /\/:id\b/.test(honoPath) || /\/:[\w]+_id\b/.test(honoPath);
|
|
362
|
+
const isList = method === 'get' && !hasIdParam;
|
|
363
|
+
|
|
364
|
+
const queryParams: string[] = [];
|
|
365
|
+
const allParams = [...(item.parameters ?? []), ...(op.parameters ?? [])];
|
|
366
|
+
for (const p of allParams) {
|
|
367
|
+
if (p.in === 'query') {
|
|
368
|
+
queryParams.push(p.name);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (!tagOps.has(tag)) tagOps.set(tag, []);
|
|
373
|
+
tagOps.get(tag)!.push({
|
|
374
|
+
method,
|
|
375
|
+
path: honoPath,
|
|
376
|
+
operationId: op.operationId,
|
|
377
|
+
summary: op.summary,
|
|
378
|
+
hasIdParam,
|
|
379
|
+
isList,
|
|
380
|
+
queryParams,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return tagOps;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function inferTagFromPath(path: string): string {
|
|
389
|
+
const segments = path.split('/').filter(Boolean);
|
|
390
|
+
// Skip path params and use first real segment
|
|
391
|
+
for (const seg of segments) {
|
|
392
|
+
if (!seg.startsWith('{')) return seg;
|
|
393
|
+
}
|
|
394
|
+
return 'default';
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function parseSpec(spec: OpenAPISpec): ParsedSpec {
|
|
398
|
+
const entities: ParsedEntity[] = [];
|
|
399
|
+
|
|
400
|
+
// Extract entities from schemas
|
|
401
|
+
if (spec.components?.schemas) {
|
|
402
|
+
for (const [name, schema] of Object.entries(spec.components.schemas)) {
|
|
403
|
+
const entity = extractEntityFromSchema(name, schema, spec);
|
|
404
|
+
if (entity) {
|
|
405
|
+
entities.push(entity);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Extract routes from paths
|
|
411
|
+
const tagOps = extractRoutes(spec);
|
|
412
|
+
const routes: ParsedRoute[] = [];
|
|
413
|
+
|
|
414
|
+
for (const [tag, operations] of tagOps) {
|
|
415
|
+
const singular = singularize(tag);
|
|
416
|
+
const pascalSingular = toPascalCase(singular);
|
|
417
|
+
const camelPlural = toCamelCase(tag);
|
|
418
|
+
|
|
419
|
+
routes.push({
|
|
420
|
+
tag,
|
|
421
|
+
filename: `${tag.replace(/_/g, '-')}.ts`,
|
|
422
|
+
functionName: `${toCamelCase(singular)}Routes`,
|
|
423
|
+
storeAccessor: camelPlural,
|
|
424
|
+
formatterName: `format${pascalSingular}`,
|
|
425
|
+
operations,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return { entities, routes };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
// Code generation
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
export function generateEntities(entities: ParsedEntity[]): string {
|
|
437
|
+
const lines: string[] = [];
|
|
438
|
+
lines.push("import type { Entity } from '../../core/index.js';");
|
|
439
|
+
lines.push('');
|
|
440
|
+
|
|
441
|
+
for (const entity of entities) {
|
|
442
|
+
lines.push(`export interface WorkOS${entity.name} extends Entity {`);
|
|
443
|
+
|
|
444
|
+
// Always include `object` field with literal type
|
|
445
|
+
const hasObjectField = entity.fields.some((f) => f.name === 'object');
|
|
446
|
+
if (hasObjectField) {
|
|
447
|
+
lines.push(` object: '${entity.objectType}';`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
for (const field of entity.fields) {
|
|
451
|
+
if (field.name === 'object') continue; // Already handled above with literal type
|
|
452
|
+
|
|
453
|
+
let tsType = field.tsType;
|
|
454
|
+
if (field.nullable && !tsType.includes('null')) {
|
|
455
|
+
tsType = `${tsType} | null`;
|
|
456
|
+
}
|
|
457
|
+
lines.push(` ${field.name}: ${tsType};`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
lines.push('}');
|
|
461
|
+
lines.push('');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return lines.join('\n');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function generateStore(entities: ParsedEntity[]): string {
|
|
468
|
+
const lines: string[] = [];
|
|
469
|
+
|
|
470
|
+
lines.push("import { type Store, type Collection } from '../../core/index.js';");
|
|
471
|
+
|
|
472
|
+
// Import entity types
|
|
473
|
+
const typeNames = entities.map((e) => `WorkOS${e.name}`);
|
|
474
|
+
if (typeNames.length > 0) {
|
|
475
|
+
lines.push('import type {');
|
|
476
|
+
for (const t of typeNames) {
|
|
477
|
+
lines.push(` ${t},`);
|
|
478
|
+
}
|
|
479
|
+
lines.push("} from './entities.js';");
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
lines.push('');
|
|
483
|
+
|
|
484
|
+
// Store interface
|
|
485
|
+
lines.push('export interface WorkOSGeneratedStore {');
|
|
486
|
+
for (const entity of entities) {
|
|
487
|
+
const accessor = toCamelCase(pluralize(entity.objectType));
|
|
488
|
+
lines.push(` ${accessor}: Collection<WorkOS${entity.name}>;`);
|
|
489
|
+
}
|
|
490
|
+
lines.push('}');
|
|
491
|
+
lines.push('');
|
|
492
|
+
|
|
493
|
+
// getWorkOSGeneratedStore function
|
|
494
|
+
lines.push('export function getWorkOSGeneratedStore(store: Store): WorkOSGeneratedStore {');
|
|
495
|
+
lines.push(' return {');
|
|
496
|
+
for (const entity of entities) {
|
|
497
|
+
const accessor = toCamelCase(pluralize(entity.objectType));
|
|
498
|
+
const namespace = `workos.${pluralize(entity.objectType)}`;
|
|
499
|
+
const indexList = entity.indexFields.map((f) => `'${f}'`).join(', ');
|
|
500
|
+
lines.push(
|
|
501
|
+
` ${accessor}: store.collection<WorkOS${entity.name}>('${namespace}', '${entity.idPrefix}', [${indexList}]),`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
lines.push(' };');
|
|
505
|
+
lines.push('}');
|
|
506
|
+
lines.push('');
|
|
507
|
+
|
|
508
|
+
return lines.join('\n');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function generateHelpers(entities: ParsedEntity[]): string {
|
|
512
|
+
const lines: string[] = [];
|
|
513
|
+
|
|
514
|
+
// Imports
|
|
515
|
+
const typeNames = entities.map((e) => `WorkOS${e.name}`);
|
|
516
|
+
lines.push('import type {');
|
|
517
|
+
for (const t of typeNames) {
|
|
518
|
+
lines.push(` ${t},`);
|
|
519
|
+
}
|
|
520
|
+
lines.push("} from './entities.js';");
|
|
521
|
+
lines.push('');
|
|
522
|
+
|
|
523
|
+
// Generate a format function for each entity
|
|
524
|
+
for (const entity of entities) {
|
|
525
|
+
const typeName = `WorkOS${entity.name}`;
|
|
526
|
+
const paramName = toCamelCase(entity.objectType);
|
|
527
|
+
const fnName = `format${entity.name}`;
|
|
528
|
+
|
|
529
|
+
lines.push(`export function ${fnName}(${paramName}: ${typeName}): Record<string, unknown> {`);
|
|
530
|
+
lines.push(' return {');
|
|
531
|
+
|
|
532
|
+
// object field
|
|
533
|
+
if (entity.fields.some((f) => f.name === 'object')) {
|
|
534
|
+
lines.push(` object: '${entity.objectType}',`);
|
|
535
|
+
}
|
|
536
|
+
lines.push(` id: ${paramName}.id,`);
|
|
537
|
+
|
|
538
|
+
for (const field of entity.fields) {
|
|
539
|
+
if (field.name === 'object') continue;
|
|
540
|
+
lines.push(` ${field.name}: ${paramName}.${field.name},`);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
lines.push(` created_at: ${paramName}.created_at,`);
|
|
544
|
+
lines.push(` updated_at: ${paramName}.updated_at,`);
|
|
545
|
+
lines.push(' };');
|
|
546
|
+
lines.push('}');
|
|
547
|
+
lines.push('');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// parseListParams helper
|
|
551
|
+
lines.push('export function parseListParams(url: URL) {');
|
|
552
|
+
lines.push(" const limit = Math.max(1, Math.min(parseInt(url.searchParams.get('limit') ?? '10'), 100));");
|
|
553
|
+
lines.push(" const order = (url.searchParams.get('order') as 'asc' | 'desc') ?? 'desc';");
|
|
554
|
+
lines.push(" const before = url.searchParams.get('before') ?? undefined;");
|
|
555
|
+
lines.push(" const after = url.searchParams.get('after') ?? undefined;");
|
|
556
|
+
lines.push(' return { limit, order, before, after };');
|
|
557
|
+
lines.push('}');
|
|
558
|
+
lines.push('');
|
|
559
|
+
|
|
560
|
+
return lines.join('\n');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function generateRoutes(route: ParsedRoute): string {
|
|
564
|
+
const lines: string[] = [];
|
|
565
|
+
|
|
566
|
+
lines.push("import { type RouteContext, notFound, validationError, parseJsonBody } from '../../../core/index.js';");
|
|
567
|
+
lines.push("import { getWorkOSGeneratedStore } from '../store.js';");
|
|
568
|
+
lines.push(`import { ${route.formatterName}, parseListParams } from '../helpers.js';`);
|
|
569
|
+
lines.push('');
|
|
570
|
+
|
|
571
|
+
lines.push(`export function ${route.functionName}(ctx: RouteContext): void {`);
|
|
572
|
+
lines.push(' const { app, store } = ctx;');
|
|
573
|
+
lines.push(' const ws = getWorkOSGeneratedStore(store);');
|
|
574
|
+
lines.push('');
|
|
575
|
+
|
|
576
|
+
for (const op of route.operations) {
|
|
577
|
+
lines.push(` // ${op.summary ?? op.operationId ?? `${op.method.toUpperCase()} ${op.path}`}`);
|
|
578
|
+
|
|
579
|
+
if (op.method === 'post') {
|
|
580
|
+
lines.push(` app.post('${op.path}', async (c) => {`);
|
|
581
|
+
lines.push(' const body = await parseJsonBody(c);');
|
|
582
|
+
lines.push('');
|
|
583
|
+
lines.push(` const item = ws.${route.storeAccessor}.insert({`);
|
|
584
|
+
lines.push(' ...body,');
|
|
585
|
+
lines.push(' });');
|
|
586
|
+
lines.push('');
|
|
587
|
+
lines.push(` return c.json(${route.formatterName}(item), 201);`);
|
|
588
|
+
lines.push(' });');
|
|
589
|
+
} else if (op.method === 'get' && op.isList) {
|
|
590
|
+
lines.push(` app.get('${op.path}', (c) => {`);
|
|
591
|
+
lines.push(' const url = new URL(c.req.url);');
|
|
592
|
+
lines.push(' const params = parseListParams(url);');
|
|
593
|
+
lines.push('');
|
|
594
|
+
lines.push(` const result = ws.${route.storeAccessor}.list({`);
|
|
595
|
+
lines.push(' ...params,');
|
|
596
|
+
lines.push(' });');
|
|
597
|
+
lines.push('');
|
|
598
|
+
lines.push(' return c.json({');
|
|
599
|
+
lines.push(" object: 'list',");
|
|
600
|
+
lines.push(` data: result.data.map(${route.formatterName}),`);
|
|
601
|
+
lines.push(' list_metadata: result.list_metadata,');
|
|
602
|
+
lines.push(' });');
|
|
603
|
+
lines.push(' });');
|
|
604
|
+
} else if (op.method === 'get' && op.hasIdParam) {
|
|
605
|
+
lines.push(` app.get('${op.path}', (c) => {`);
|
|
606
|
+
lines.push(` const item = ws.${route.storeAccessor}.get(c.req.param('id'));`);
|
|
607
|
+
lines.push(` if (!item) throw notFound('${toPascalCase(singularize(route.tag))}');`);
|
|
608
|
+
lines.push(` return c.json(${route.formatterName}(item));`);
|
|
609
|
+
lines.push(' });');
|
|
610
|
+
} else if (op.method === 'put' && op.hasIdParam) {
|
|
611
|
+
lines.push(` app.put('${op.path}', async (c) => {`);
|
|
612
|
+
lines.push(` const item = ws.${route.storeAccessor}.get(c.req.param('id'));`);
|
|
613
|
+
lines.push(` if (!item) throw notFound('${toPascalCase(singularize(route.tag))}');`);
|
|
614
|
+
lines.push('');
|
|
615
|
+
lines.push(' const body = await parseJsonBody(c);');
|
|
616
|
+
lines.push(` const updated = ws.${route.storeAccessor}.update(item.id, body);`);
|
|
617
|
+
lines.push(` return c.json(${route.formatterName}(updated!));`);
|
|
618
|
+
lines.push(' });');
|
|
619
|
+
} else if (op.method === 'patch' && op.hasIdParam) {
|
|
620
|
+
lines.push(` app.patch('${op.path}', async (c) => {`);
|
|
621
|
+
lines.push(` const item = ws.${route.storeAccessor}.get(c.req.param('id'));`);
|
|
622
|
+
lines.push(` if (!item) throw notFound('${toPascalCase(singularize(route.tag))}');`);
|
|
623
|
+
lines.push('');
|
|
624
|
+
lines.push(' const body = await parseJsonBody(c);');
|
|
625
|
+
lines.push(` const updated = ws.${route.storeAccessor}.update(item.id, body);`);
|
|
626
|
+
lines.push(` return c.json(${route.formatterName}(updated!));`);
|
|
627
|
+
lines.push(' });');
|
|
628
|
+
} else if (op.method === 'delete' && op.hasIdParam) {
|
|
629
|
+
lines.push(` app.delete('${op.path}', (c) => {`);
|
|
630
|
+
lines.push(` const item = ws.${route.storeAccessor}.get(c.req.param('id'));`);
|
|
631
|
+
lines.push(` if (!item) throw notFound('${toPascalCase(singularize(route.tag))}');`);
|
|
632
|
+
lines.push(` ws.${route.storeAccessor}.delete(item.id);`);
|
|
633
|
+
lines.push(' return c.body(null, 204);');
|
|
634
|
+
lines.push(' });');
|
|
635
|
+
} else {
|
|
636
|
+
// Fallback: generate a TODO stub
|
|
637
|
+
lines.push(` // TODO: implement ${op.method.toUpperCase()} ${op.path}`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
lines.push('');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
lines.push('}');
|
|
644
|
+
lines.push('');
|
|
645
|
+
|
|
646
|
+
return lines.join('\n');
|
|
647
|
+
}
|