Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API migration routes and helpers #367

Draft
wants to merge 51 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
1feebb8
added generate schema
fomalhautb Dec 6, 2024
6ae7af6
migration
fomalhautb Dec 7, 2024
90fe7dc
Merge branch 'dev' into api-migration
fomalhautb Dec 9, 2024
76d399f
fixed migration
fomalhautb Dec 9, 2024
0fed49a
updated schema gen
fomalhautb Dec 10, 2024
861ad4e
type schema gen
fomalhautb Dec 10, 2024
5878bab
used prettier for formatting
fomalhautb Dec 10, 2024
e4739c6
added import gen
fomalhautb Dec 10, 2024
ac00236
Merge branch 'dev' into api-migration
fomalhautb Dec 10, 2024
83b99bc
schema gen
fomalhautb Dec 11, 2024
84daece
added url match
fomalhautb Dec 11, 2024
194b4ea
created migration route
fomalhautb Dec 11, 2024
520b792
Merge branch 'dev' into api-migration
fomalhautb Dec 11, 2024
932a025
updated create migration route
fomalhautb Dec 11, 2024
99a0198
added more parsing code
fomalhautb Dec 12, 2024
596aa53
added yup type gen
fomalhautb Dec 12, 2024
8fc63ba
added response type
fomalhautb Dec 12, 2024
1d6f367
added more convertion files
fomalhautb Dec 12, 2024
fe29802
implemented convertion
fomalhautb Dec 12, 2024
da07852
Merge branch 'dev' into api-migration
fomalhautb Dec 12, 2024
af0f135
Merge branch 'dev' into api-migration
fomalhautb Dec 12, 2024
62e25cc
added more schemas
fomalhautb Dec 12, 2024
f94dbe6
update
fomalhautb Dec 12, 2024
8ee10b2
added create endpoint handlers from newest endpoints
fomalhautb Dec 12, 2024
4561f58
fixed types
fomalhautb Dec 12, 2024
37e2104
added test file
fomalhautb Dec 13, 2024
37934c5
fixed migration route
fomalhautb Dec 13, 2024
5d64a7e
updated code format
fomalhautb Dec 13, 2024
4b1599c
updated small things
fomalhautb Dec 13, 2024
b348299
renamed file
fomalhautb Dec 13, 2024
c121c03
improved code structure
fomalhautb Dec 13, 2024
bf956df
added migration handler
fomalhautb Dec 13, 2024
63ada02
added more test code
fomalhautb Dec 13, 2024
cc062e6
Merge branch 'dev' into api-migration
fomalhautb Dec 15, 2024
c30e215
fixed migration route type
fomalhautb Dec 15, 2024
28f9bd6
fixed type
fomalhautb Dec 17, 2024
7260ec4
improved types
fomalhautb Dec 17, 2024
82086bd
new schema
fomalhautb Dec 17, 2024
6ccf271
generate schema
fomalhautb Dec 17, 2024
65fc682
fixed bugs
fomalhautb Dec 17, 2024
ec10dff
updated migration handler
fomalhautb Dec 17, 2024
288fc2c
Merge branch 'dev' into api-migration
fomalhautb Dec 17, 2024
f5a2a28
Merge branch 'dev' into api-migration
fomalhautb Dec 19, 2024
6d8d315
Merge branch 'dev' into api-migration
fomalhautb Dec 20, 2024
db84052
Merge branch 'dev' into api-migration
fomalhautb Dec 21, 2024
fb0af98
Merge branch 'dev' into api-migration
fomalhautb Dec 23, 2024
d89ae9f
Merge branch 'dev' into api-migration
fomalhautb Dec 24, 2024
abecb7d
updated pnpm lock
fomalhautb Dec 24, 2024
db96ef8
Merge branch 'dev' into api-migration
N2D4 Dec 27, 2024
e53a28e
Merge branch 'dev' into api-migration
fomalhautb Jan 7, 2025
9be2700
Merge branch 'dev' into api-migration
fomalhautb Jan 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"openid-client": "^5.6.4",
"oslo": "^1.2.1",
"posthog-node": "^4.1.0",
"prettier": "^3.4.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"semver": "^7.6.3",
Expand Down
25 changes: 2 additions & 23 deletions apps/backend/scripts/generate-docs.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,15 @@
import { listEndpoints } from '@/lib/glob';
import { parseOpenAPI, parseWebhookOpenAPI } from '@/lib/openapi';
import { isSmartRouteHandler } from '@/route-handlers/smart-route-handler';
import { webhookEvents } from '@stackframe/stack-shared/dist/interface/webhooks';
import { HTTP_METHODS } from '@stackframe/stack-shared/dist/utils/http';
import { typedKeys } from '@stackframe/stack-shared/dist/utils/objects';
import fs from 'fs';
import { glob } from 'glob';
import path from 'path';
import yaml from 'yaml';

async function main() {
console.log("Started docs schema generator");

for (const audience of ['client', 'server', 'admin'] as const) {
const filePathPrefix = path.resolve(process.platform === "win32" ? "apps/src/app/api/v1" : "src/app/api/v1");
const importPathPrefix = "@/app/api/v1";
const filePaths = [...await glob(filePathPrefix + "/**/route.{js,jsx,ts,tsx}")];
const openAPISchema = yaml.stringify(parseOpenAPI({
endpoints: new Map(await Promise.all(filePaths.map(async (filePath) => {
if (!filePath.startsWith(filePathPrefix)) {
throw new Error(`Invalid file path: ${filePath}`);
}
const suffix = filePath.slice(filePathPrefix.length);
const midfix = suffix.slice(0, suffix.lastIndexOf("/route."));
const importPath = `${importPathPrefix}${suffix}`;
const urlPath = midfix.replaceAll("[", "{").replaceAll("]", "}");
const myModule = require(importPath);
const handlersByMethod = new Map(
typedKeys(HTTP_METHODS).map(method => [method, myModule[method]] as const)
.filter(([_, handler]) => isSmartRouteHandler(handler))
);
return [urlPath, handlersByMethod] as const;
}))),
endpoints: await listEndpoints("api/v1", true, true),
audience,
}));
fs.writeFileSync(`../../docs/fern/openapi/${audience}.yaml`, openAPISchema);
Expand Down
214 changes: 214 additions & 0 deletions apps/backend/scripts/generate-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { listEndpoints } from '@/lib/glob';
import { isSmartRouteHandler } from '@/route-handlers/smart-route-handler';
import fs from 'fs';
import prettier from 'prettier';
import * as yup from 'yup';

function convertUrlToJSVariable(url: string, method: string) {
return 'i' + url.replaceAll('[', '')
.replaceAll(']', '')
.replaceAll('.', '')
.replaceAll('/', '_')
.replaceAll('-', '_')
.replace(/_[a-z]/g, match => match[1].toUpperCase())
.replace(/^[a-z]/, match => match.toUpperCase())
+ method.slice(0, 1).toUpperCase() + method.slice(1).toLowerCase();
}

async function main() {
console.log("Started docs schema generator");

const endpoints = await listEndpoints("api/v1", false);

// ========== generate schema.ts ==========
let schemaContent = 'import { yupObject, yupArray, yupString, yupNumber, yupBoolean, yupMixed } from "@stackframe/stack-shared/dist/schema-fields";\n\n';
schemaContent += 'export const endpointSchema = {';

endpoints.forEach((handlersByMethod, url) => {
let methodContent = '{';

handlersByMethod.forEach((handler, method) => {
if (!isSmartRouteHandler(handler)) {
// TODO: handle non-smart route handlers
console.warn(`Skipping non-smart route handler: ${url} ${method}`);
return;
}

const audiences = new Map<string, any>();
for (const overload of handler.overloads.values()) {
for (const audience of ['client', 'server', 'admin'] as const) {
const schemaAudience = (overload.request.describe() as any).fields.auth?.fields?.type;
if (!schemaAudience) continue;
if ("oneOf" in schemaAudience && schemaAudience.oneOf.length > 0 && schemaAudience.oneOf.includes(audience)) {
audiences.set(audience, overload);
}
}
}

if (handler.overloads.size !== 1 && audiences.size !== handler.overloads.size) {
throw new Error(`Expected ${handler.overloads.size} audiences, got ${audiences.size}. Multiple audiences other than client, server, and admin is currently not supported. You might need to manually fix this.`);
}

let endpointContent;
if (audiences.size === 0) {
endpointContent = '{default: ' + endpointSchemaToTypeString(
handler.overloads.values().next().value.request.describe(),
handler.overloads.values().next().value.response.describe()
) + '}';
} else {
endpointContent = '{' + Array.from(audiences.entries()).map(([audience, overload]) => {
return `${audience}: ${endpointSchemaToTypeString(overload.request.describe(), overload.response.describe())}`;
}).join(',') + '}';
}

methodContent += `${method}: ${endpointContent},`;
});

methodContent += '}';
schemaContent += `"${url || '/'}": ${methodContent},`;
});

schemaContent += '}';

// ========== generate imports.ts ==========
let importHeaders = '';
let importBody = 'export const endpoints = {';

endpoints.forEach((handlersByMethod, url) => {
let methodBody = '';
for (const method of handlersByMethod.keys()) {
importHeaders += `import { ${method} as ${convertUrlToJSVariable(url, method)} } from "../../api/v1${url}/route";\n`;
methodBody += `"${method}": ${convertUrlToJSVariable(url, method)},`;
}
importBody += `"${url || '/'}": {${methodBody}},\n`;
});

importBody += '}';
// ========================================

const prettierConfig = {
parser: "typescript",
semi: true,
singleQuote: true,
trailingComma: "all",
} as const;

fs.writeFileSync('src/app/api/v2/schema.ts', await prettier.format(schemaContent, prettierConfig));
fs.writeFileSync('src/app/api/v2/imports.ts', await prettier.format(importHeaders + '\n' + importBody, prettierConfig));
}

function endpointSchemaToTypeString(reqSchema: yup.SchemaFieldDescription, resSchema: yup.SchemaFieldDescription): string {
if (reqSchema.type !== 'object') {
throw new Error(`Unsupported schema type: ${reqSchema.type}`);
}
let inputFields = "{";
for (const key of ['body', 'query', 'params']) {
const field = Object.entries((reqSchema as any).fields).find(([k]) => k === key);
if (field && Object.keys((field[1] as any).fields || {}).length > 0) {
inputFields += `${key}: ${schemaToTypeString(field[1] as any)},`;
}
}
inputFields += "}";

let outputFields = "{";
const rawOutputFields = (resSchema as any).fields;
if (rawOutputFields) {
for (const key of ['statusCode', 'bodyType', 'headers', 'body']) {
const field = Object.entries(rawOutputFields).find(([k]) => k === key);
if (field) {
if (key === 'statusCode') {
outputFields += `statusCode: [${(field[1] as any).oneOf.join(',')}],`;
} else if (key === 'bodyType') {
const bodyType = (field[1] as any).oneOf;
if (bodyType.length !== 1) {
throw new Error(`Unsupported body type: ${bodyType}`);
}
outputFields += `bodyType: "${bodyType[0]}",`;
} else {
outputFields += `${key}: ${schemaToTypeString(field[1] as any)},`;
}
}
}
}
outputFields += "}";

return `{
input: ${inputFields},
output: ${outputFields},
}`;
}

function schemaToTypeString(schema: yup.SchemaFieldDescription): string {
let result;
switch (schema.type) {
case 'object': {
result = `yupObject({${Object.entries((schema as any).fields).map(([key, value]): any => `"${key}": ${schemaToTypeString(value as any)}`).join(',')}})`;
break;
}
case 'array': {
result = `yupArray(${schemaToTypeString((schema as any).innerType)})`;
break;
}
case 'tuple': {
result = `yupTuple([${(schema as any).innerType.map((value: any) => schemaToTypeString(value)).join(',')}])`;
break;
}
case 'mixed': {
result = 'yupMixed()';
break;
}
case 'string': {
result = 'yupString()';
break;
}
case 'number': {
result = 'yupNumber()';
break;
}
case 'boolean': {
result = 'yupBoolean()';
break;
}
default: {
throw new Error(`Unsupported schema type: ${schema.type}`);
}
}

const optional = (schema as any).optional;
const nullable = (schema as any).nullable;

if (!optional && !nullable) {
result += '.defined()';
}

if (optional) {
result += '.optional()';
}

if (nullable) {
result += '.nullable()';
}

if ((schema as any).oneOf && (schema as any).oneOf.length > 0 && (schema as any).oneOf.every((value: any) => value !== undefined)) {
result += `.oneOf([${(schema as any).oneOf
.map((value: any) => {
if (typeof value === 'string') {
return `"${value}"`;
} else if (typeof value === 'boolean') {
return value ? 'true' : 'false';
} else if (typeof value === 'number') {
return value.toString();
} else {
throw new Error(`Unsupported oneOf value: ${value}`);
}
})
.join(',')}])`;
}

return result;
}

main().catch((...args) => {
console.error(`ERROR! Could not generate schema`, ...args);
process.exit(1);
});
Loading
Loading