refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -0,0 +1,168 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {SnowflakeTypeRef} from '@fluxer/openapi/src/converters/BuiltInSchemas';
import type {OpenAPIParameter} from '@fluxer/openapi/src/Types';
export const COMMON_PATH_PARAMETERS: Record<string, OpenAPIParameter> = {
guild_id: {
name: 'guild_id',
in: 'path',
required: true,
schema: SnowflakeTypeRef,
description: 'The ID of the guild',
},
channel_id: {
name: 'channel_id',
in: 'path',
required: true,
schema: SnowflakeTypeRef,
description: 'The ID of the channel',
},
user_id: {
name: 'user_id',
in: 'path',
required: true,
schema: SnowflakeTypeRef,
description: 'The ID of the user',
},
message_id: {
name: 'message_id',
in: 'path',
required: true,
schema: SnowflakeTypeRef,
description: 'The ID of the message',
},
role_id: {
name: 'role_id',
in: 'path',
required: true,
schema: SnowflakeTypeRef,
description: 'The ID of the role',
},
emoji_id: {
name: 'emoji_id',
in: 'path',
required: true,
schema: SnowflakeTypeRef,
description: 'The ID of the emoji',
},
sticker_id: {
name: 'sticker_id',
in: 'path',
required: true,
schema: SnowflakeTypeRef,
description: 'The ID of the sticker',
},
invite_code: {
name: 'invite_code',
in: 'path',
required: true,
schema: {type: 'string'},
description: 'The invite code',
},
webhook_id: {
name: 'webhook_id',
in: 'path',
required: true,
schema: SnowflakeTypeRef,
description: 'The ID of the webhook',
},
webhook_token: {
name: 'webhook_token',
in: 'path',
required: true,
schema: {type: 'string'},
description: 'The webhook token',
},
pack_id: {
name: 'pack_id',
in: 'path',
required: true,
schema: SnowflakeTypeRef,
description: 'The ID of the pack',
},
application_id: {
name: 'application_id',
in: 'path',
required: true,
schema: SnowflakeTypeRef,
description: 'The ID of the OAuth2 application',
},
};
export const COMMON_QUERY_PARAMETERS: Record<string, OpenAPIParameter> = {
limit: {
name: 'limit',
in: 'query',
required: false,
schema: {type: 'integer', minimum: 1, maximum: 100, default: 50},
description: 'Maximum number of results to return',
},
before: {
name: 'before',
in: 'query',
required: false,
schema: SnowflakeTypeRef,
description: 'Get results before this ID',
},
after: {
name: 'after',
in: 'query',
required: false,
schema: SnowflakeTypeRef,
description: 'Get results after this ID',
},
around: {
name: 'around',
in: 'query',
required: false,
schema: SnowflakeTypeRef,
description: 'Get results around this ID',
},
};
export function extractPathParameters(path: string): Array<OpenAPIParameter> {
const paramRegex = /:(\w+)/g;
const parameters: Array<OpenAPIParameter> = [];
let match: RegExpExecArray | null;
while ((match = paramRegex.exec(path)) !== null) {
const paramName = match[1];
const commonParam = COMMON_PATH_PARAMETERS[paramName];
if (commonParam) {
parameters.push({...commonParam});
} else {
parameters.push({
name: paramName,
in: 'path',
required: true,
schema: {type: 'string'},
description: `The ${paramName.replace(/_/g, ' ')}`,
});
}
}
return parameters;
}
export function convertPathToOpenAPI(path: string): string {
return path.replace(/:(\w+)/g, '{$1}');
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {OpenAPIResponse, OpenAPISchema} from '@fluxer/openapi/src/Types';
export const ERROR_SCHEMA: OpenAPISchema = {
type: 'object',
properties: {
code: {
$ref: '#/components/schemas/APIErrorCode',
},
message: {
type: 'string',
description: 'Human-readable error message',
},
errors: {
type: 'array',
description: 'Field-specific validation errors',
items: {
$ref: '#/components/schemas/ValidationErrorItem',
},
},
},
required: ['code', 'message'],
};
export const RATE_LIMIT_HEADERS: Record<string, {description: string; schema: OpenAPISchema}> = {
'X-RateLimit-Limit': {
description: 'The number of requests that can be made in the current window',
schema: {type: 'integer'},
},
'X-RateLimit-Remaining': {
description: 'The number of remaining requests that can be made',
schema: {type: 'integer'},
},
'X-RateLimit-Reset': {
description: 'Unix timestamp when the rate limit resets',
schema: {type: 'integer'},
},
'Retry-After': {
description: 'Number of seconds to wait before retrying (only on 429)',
schema: {type: 'integer'},
},
};
export const COMMON_RESPONSES: Record<string, OpenAPIResponse> = {
'400': {
description: 'Bad Request - The request was malformed or contained invalid data',
content: {
'application/json': {
schema: {$ref: '#/components/schemas/Error'},
},
},
},
'401': {
description: 'Unauthorized - Authentication is required or the token is invalid',
content: {
'application/json': {
schema: {$ref: '#/components/schemas/Error'},
},
},
},
'403': {
description: 'Forbidden - You do not have permission to perform this action',
content: {
'application/json': {
schema: {$ref: '#/components/schemas/Error'},
},
},
},
'404': {
description: 'Not Found - The requested resource was not found',
content: {
'application/json': {
schema: {$ref: '#/components/schemas/Error'},
},
},
},
'429': {
description: 'Too Many Requests - You are being rate limited',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
code: {type: 'string', enum: ['RATE_LIMITED']},
message: {type: 'string'},
retry_after: {type: 'number', description: 'Seconds to wait before retrying'},
global: {type: 'boolean', description: 'Whether this is a global rate limit'},
},
required: ['code', 'message', 'retry_after'],
},
},
},
headers: {
'Retry-After': RATE_LIMIT_HEADERS['Retry-After'],
'X-RateLimit-Limit': RATE_LIMIT_HEADERS['X-RateLimit-Limit'],
'X-RateLimit-Remaining': RATE_LIMIT_HEADERS['X-RateLimit-Remaining'],
'X-RateLimit-Reset': RATE_LIMIT_HEADERS['X-RateLimit-Reset'],
},
},
'500': {
description: 'Internal Server Error - An unexpected error occurred',
content: {
'application/json': {
schema: {$ref: '#/components/schemas/Error'},
},
},
},
};
export function getErrorResponses(requiresAuth: boolean): Record<string, OpenAPIResponse> {
const responses: Record<string, OpenAPIResponse> = {
'400': COMMON_RESPONSES['400'],
'429': COMMON_RESPONSES['429'],
'500': COMMON_RESPONSES['500'],
};
if (requiresAuth) {
responses['401'] = COMMON_RESPONSES['401'];
responses['403'] = COMMON_RESPONSES['403'];
}
return responses;
}
export function getSuccessResponse(schemaRef: string | null): OpenAPIResponse {
if (!schemaRef) {
return {
description: 'Success',
};
}
return {
description: 'Success',
content: {
'application/json': {
schema: {$ref: schemaRef},
},
},
};
}
export function getNoContentResponse(): OpenAPIResponse {
return {
description: 'No Content',
};
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import {setSchemaName, zodToOpenAPISchema} from '@fluxer/openapi/src/converters/ZodToOpenAPI';
import type {OpenAPISchema} from '@fluxer/openapi/src/Types';
import type {z} from 'zod';
export interface LoadedSchema {
name: string;
zodSchema: z.ZodTypeAny;
openAPISchema: OpenAPISchema;
}
function discoverSchemaModules(rootDir: string): Array<string> {
if (!fs.existsSync(rootDir)) {
return [];
}
const results: Array<string> = [];
const stack: Array<string> = [rootDir];
while (stack.length > 0) {
const currentDir = stack.pop();
if (!currentDir) break;
const entries = fs.readdirSync(currentDir, {withFileTypes: true});
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
if (entry.name === 'tests' || entry.name === 'node_modules') continue;
stack.push(fullPath);
continue;
}
if (!entry.isFile()) continue;
if (!entry.name.endsWith('.tsx')) continue;
if (entry.name.endsWith('.test.tsx')) continue;
results.push(fullPath);
}
}
return results.sort();
}
function getModulePaths(basePath: string): Array<string> {
const schemaDomains = path.join(basePath, 'packages', 'schema', 'src', 'domains');
return discoverSchemaModules(schemaDomains);
}
function isZodSchema(value: unknown): value is z.ZodTypeAny {
return (
value !== null &&
typeof value === 'object' &&
'_def' in value &&
typeof (value as {parse?: unknown}).parse === 'function'
);
}
export async function loadSchemas(basePath: string): Promise<Map<string, LoadedSchema>> {
const schemas = new Map<string, LoadedSchema>();
const collectedSchemas: Array<{name: string; zodSchema: z.ZodTypeAny}> = [];
const modulePaths = getModulePaths(basePath);
for (const modulePath of modulePaths) {
try {
const moduleExports = await import(modulePath);
for (const [exportName, exportValue] of Object.entries(moduleExports)) {
if (exportName.startsWith('_')) {
continue;
}
if (typeof exportValue === 'function') {
continue;
}
if (isZodSchema(exportValue)) {
collectedSchemas.push({name: exportName, zodSchema: exportValue});
}
}
} catch (error) {
console.warn(`Warning: Could not load schema module ${modulePath}:`, error);
}
}
for (const {name, zodSchema} of collectedSchemas) {
setSchemaName(zodSchema, name);
}
for (const {name, zodSchema} of collectedSchemas) {
const openAPISchemaOrRef = zodToOpenAPISchema(zodSchema);
if ('$ref' in openAPISchemaOrRef) {
throw new Error(`Top-level schema export must not be a $ref: ${name}`);
}
const openAPISchema: OpenAPISchema = openAPISchemaOrRef;
schemas.set(name, {
name,
zodSchema,
openAPISchema,
});
}
return schemas;
}
export function getSchemaNames(basePath: string): Array<string> {
const modulePaths = getModulePaths(basePath);
const names: Array<string> = [];
for (const modulePath of modulePaths) {
const parts = modulePath.split('/');
const fileName = parts[parts.length - 1];
names.push(fileName);
}
return names;
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {OpenAPIRef, OpenAPISchema} from '@fluxer/openapi/src/Types';
export class SchemaRegistry {
private schemas: Map<string, OpenAPISchema> = new Map();
private references: Set<string> = new Set();
register(name: string, schema: OpenAPISchema): void {
this.schemas.set(name, schema);
}
getRef(name: string): OpenAPIRef {
this.references.add(name);
return {$ref: `#/components/schemas/${name}`};
}
has(name: string): boolean {
return this.schemas.has(name);
}
get(name: string): OpenAPISchema | undefined {
return this.schemas.get(name);
}
getAllSchemas(): Record<string, OpenAPISchema> {
const result: Record<string, OpenAPISchema> = {};
for (const [name, schema] of this.schemas) {
result[name] = schema;
}
return result;
}
getReferencedSchemas(): Record<string, OpenAPISchema> {
const result: Record<string, OpenAPISchema> = {};
for (const name of this.references) {
const schema = this.schemas.get(name);
if (schema) {
result[name] = schema;
}
}
return result;
}
getUnreferencedSchemas(): Array<string> {
const unreferenced: Array<string> = [];
for (const name of this.schemas.keys()) {
if (!this.references.has(name)) {
unreferenced.push(name);
}
}
return unreferenced;
}
clear(): void {
this.schemas.clear();
this.references.clear();
}
}
export const globalSchemaRegistry = new SchemaRegistry();