@meridianjs/framework 0.1.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/dist/index.d.ts +195 -0
- package/dist/index.js +735 -0
- package/package.json +47 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { Express, RequestHandler, Request, Response, NextFunction } from 'express';
|
|
3
|
+
import { MeridianContainer, MeridianConfig, ModuleConfig, ModuleDefinition, PluginConfig, ILogger } from '@meridianjs/types';
|
|
4
|
+
import * as express_rate_limit from 'express-rate-limit';
|
|
5
|
+
import { ZodSchema } from 'zod';
|
|
6
|
+
export { asClass, asFunction, asValue } from 'awilix';
|
|
7
|
+
|
|
8
|
+
interface BootstrapOptions {
|
|
9
|
+
/** Absolute path to the project root directory */
|
|
10
|
+
rootDir: string;
|
|
11
|
+
/** Path to meridian.config.ts — defaults to <rootDir>/meridian.config.ts */
|
|
12
|
+
configPath?: string;
|
|
13
|
+
}
|
|
14
|
+
interface MeridianApp {
|
|
15
|
+
container: MeridianContainer;
|
|
16
|
+
server: Express;
|
|
17
|
+
httpServer: http.Server;
|
|
18
|
+
start(): Promise<void>;
|
|
19
|
+
stop(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Bootstraps a Meridian application.
|
|
23
|
+
*
|
|
24
|
+
* Order of operations:
|
|
25
|
+
* 1. Load meridian.config.ts
|
|
26
|
+
* 2. Create global DI container
|
|
27
|
+
* 3. Register core primitives (logger, config)
|
|
28
|
+
* 4. Register infrastructure modules (event bus, scheduler)
|
|
29
|
+
* 5. Load domain modules from config.modules[]
|
|
30
|
+
* 6. Load plugins from config.plugins[]
|
|
31
|
+
* 7. Load module links from src/links/
|
|
32
|
+
* 8. Create Express server
|
|
33
|
+
* 9. Apply middlewares from src/api/middlewares.ts
|
|
34
|
+
* 10. Load file-based API routes from src/api/
|
|
35
|
+
* 11. Load subscribers from src/subscribers/
|
|
36
|
+
* 12. Load scheduled jobs from src/jobs/
|
|
37
|
+
*/
|
|
38
|
+
declare function bootstrap(opts: BootstrapOptions): Promise<MeridianApp>;
|
|
39
|
+
|
|
40
|
+
declare function createMeridianContainer(): MeridianContainer;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Loads and validates the user's meridian.config.ts (or .js / .mjs) file.
|
|
44
|
+
* Supports both ESM and CJS projects.
|
|
45
|
+
*/
|
|
46
|
+
declare function loadConfig(rootDir: string, configPath?: string): Promise<MeridianConfig>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Type-safe helper for defining a Meridian configuration.
|
|
50
|
+
* Returns the config unchanged — purely for IDE autocomplete and type checking.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // meridian.config.ts
|
|
54
|
+
* import { defineConfig } from "@meridianjs/framework"
|
|
55
|
+
*
|
|
56
|
+
* export default defineConfig({
|
|
57
|
+
* projectConfig: {
|
|
58
|
+
* databaseUrl: process.env.DATABASE_URL!,
|
|
59
|
+
* jwtSecret: process.env.JWT_SECRET!,
|
|
60
|
+
* },
|
|
61
|
+
* modules: [
|
|
62
|
+
* { resolve: "@meridianjs/user" },
|
|
63
|
+
* { resolve: "@meridianjs/project" },
|
|
64
|
+
* ],
|
|
65
|
+
* })
|
|
66
|
+
*/
|
|
67
|
+
declare function defineConfig(config: MeridianConfig): MeridianConfig;
|
|
68
|
+
|
|
69
|
+
interface MiddlewareRoute {
|
|
70
|
+
matcher: string | RegExp;
|
|
71
|
+
middlewares: RequestHandler[];
|
|
72
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "ALL";
|
|
73
|
+
}
|
|
74
|
+
interface MiddlewaresConfig {
|
|
75
|
+
routes: MiddlewareRoute[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Type-safe helper for defining API middleware configuration.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* // src/api/middlewares.ts
|
|
82
|
+
* import { defineMiddlewares } from "@meridianjs/framework"
|
|
83
|
+
* import { authenticateJWT } from "@meridianjs/auth"
|
|
84
|
+
*
|
|
85
|
+
* export default defineMiddlewares({
|
|
86
|
+
* routes: [
|
|
87
|
+
* { matcher: "/admin*", middlewares: [authenticateJWT()] },
|
|
88
|
+
* ],
|
|
89
|
+
* })
|
|
90
|
+
*/
|
|
91
|
+
declare function defineMiddlewares(config: MiddlewaresConfig): MiddlewaresConfig;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Loads all modules declared in the config's modules[] array.
|
|
95
|
+
*
|
|
96
|
+
* Each module gets its own isolated child container (Awilix scope).
|
|
97
|
+
* Internal services (e.g. sub-services) remain private to the module.
|
|
98
|
+
* Only the module's main service is promoted to the global container.
|
|
99
|
+
*/
|
|
100
|
+
declare function loadModules(globalContainer: MeridianContainer, moduleConfigs: ModuleConfig[], rootDir: string): Promise<void>;
|
|
101
|
+
declare function resolveModuleDefinition(config: ModuleConfig, rootDir: string): Promise<ModuleDefinition>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Scans src/api/ recursively for route.ts files and registers them on Express.
|
|
105
|
+
*
|
|
106
|
+
* File path → Express route:
|
|
107
|
+
* src/api/admin/projects/route.ts → /admin/projects
|
|
108
|
+
* src/api/admin/projects/[id]/route.ts → /admin/projects/:id
|
|
109
|
+
* src/api/store/products/route.ts → /store/products
|
|
110
|
+
*
|
|
111
|
+
* Each route.ts exports named HTTP method handlers:
|
|
112
|
+
* export const GET = async (req, res) => { ... }
|
|
113
|
+
* export const POST = async (req, res) => { ... }
|
|
114
|
+
*/
|
|
115
|
+
declare function loadRoutes(app: Express, container: MeridianContainer, apiDir: string): Promise<void>;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Scans src/subscribers/ for subscriber files and registers them on the event bus.
|
|
119
|
+
*
|
|
120
|
+
* Each subscriber file must export:
|
|
121
|
+
* - default: async function handler({ event, container }) { ... }
|
|
122
|
+
* - config: { event: "event.name" | string[] }
|
|
123
|
+
*/
|
|
124
|
+
declare function loadSubscribers(container: MeridianContainer, subscribersDir: string): Promise<void>;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Scans src/jobs/ for scheduled job files and registers them with the scheduler.
|
|
128
|
+
*
|
|
129
|
+
* Each job file must export:
|
|
130
|
+
* - default: async function(container) { ... }
|
|
131
|
+
* - config: { name: string, schedule: string | { interval: number } }
|
|
132
|
+
*/
|
|
133
|
+
declare function loadJobs(container: MeridianContainer, jobsDir: string): Promise<void>;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Scans src/links/ for link definition files, registers a LinkService and
|
|
137
|
+
* a Query service in the global container.
|
|
138
|
+
*
|
|
139
|
+
* Link files export a default LinkDefinition created with defineLink().
|
|
140
|
+
*/
|
|
141
|
+
declare function loadLinks(container: MeridianContainer, linksDir: string): Promise<void>;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Loads plugins declared in config.plugins[].
|
|
145
|
+
*
|
|
146
|
+
* Plugin package contract:
|
|
147
|
+
* - Default export: async register(ctx: PluginRegistrationContext) — optional
|
|
148
|
+
* - Named export `pluginRoot`: string — path to the directory containing
|
|
149
|
+
* the plugin's api/, subscribers/, jobs/ sub-directories.
|
|
150
|
+
* In production (compiled): points to the plugin's dist/ dir.
|
|
151
|
+
* In development (tsx): points to the plugin's src/ dir.
|
|
152
|
+
*
|
|
153
|
+
* Auto-scan order when `pluginRoot` is NOT exported:
|
|
154
|
+
* 1. <packageRoot>/dist/ — built output
|
|
155
|
+
* 2. <packageRoot>/src/ — TypeScript source (dev with tsx)
|
|
156
|
+
* 3. <resolvedPath>/ — local path plugins
|
|
157
|
+
*
|
|
158
|
+
* Supported resolve values in config:
|
|
159
|
+
* - npm package name: "@my-org/meridian-github"
|
|
160
|
+
* - relative local path: "./src/plugins/my-plugin"
|
|
161
|
+
* - absolute path: "/absolute/path/to/plugin"
|
|
162
|
+
*/
|
|
163
|
+
declare function loadPlugins(container: MeridianContainer, plugins: PluginConfig[], rootDir: string): Promise<void>;
|
|
164
|
+
|
|
165
|
+
declare class ConsoleLogger implements ILogger {
|
|
166
|
+
private readonly prefix;
|
|
167
|
+
constructor(prefix?: string);
|
|
168
|
+
private format;
|
|
169
|
+
info(message: string, meta?: Record<string, unknown>): void;
|
|
170
|
+
warn(message: string, meta?: Record<string, unknown>): void;
|
|
171
|
+
error(message: string, meta?: Record<string, unknown>): void;
|
|
172
|
+
debug(message: string, meta?: Record<string, unknown>): void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
declare function createServer(container: MeridianContainer, config: MeridianConfig): Express;
|
|
176
|
+
|
|
177
|
+
/** Strict limiter for auth endpoints: 10 requests per minute per IP. */
|
|
178
|
+
declare const authRateLimit: express_rate_limit.RateLimitRequestHandler;
|
|
179
|
+
/** General API limiter: 300 requests per minute per IP. */
|
|
180
|
+
declare const apiRateLimit: express_rate_limit.RateLimitRequestHandler;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Express middleware that validates `req.body` against a Zod schema.
|
|
184
|
+
*
|
|
185
|
+
* On success, replaces `req.body` with the parsed (coerced/stripped) value
|
|
186
|
+
* and calls `next()`.
|
|
187
|
+
* On failure, responds 400 with a structured error including per-field details.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* const loginSchema = z.object({ email: z.string().email(), password: z.string().min(1) })
|
|
191
|
+
* export const POST = [validate(loginSchema), handler]
|
|
192
|
+
*/
|
|
193
|
+
declare function validate(schema: ZodSchema): (req: Request, res: Response, next: NextFunction) => void;
|
|
194
|
+
|
|
195
|
+
export { type BootstrapOptions, ConsoleLogger, type MeridianApp, type MiddlewareRoute, type MiddlewaresConfig, apiRateLimit, authRateLimit, bootstrap, createMeridianContainer, createServer, defineConfig, defineMiddlewares, loadConfig, loadJobs, loadLinks, loadModules, loadPlugins, loadRoutes, loadSubscribers, resolveModuleDefinition, validate };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
// src/bootstrap.ts
|
|
2
|
+
import path8 from "path";
|
|
3
|
+
import http from "http";
|
|
4
|
+
|
|
5
|
+
// src/container.ts
|
|
6
|
+
import {
|
|
7
|
+
createContainer,
|
|
8
|
+
asValue,
|
|
9
|
+
asClass,
|
|
10
|
+
asFunction,
|
|
11
|
+
InjectionMode
|
|
12
|
+
} from "awilix";
|
|
13
|
+
function wrapContainer(raw) {
|
|
14
|
+
const container = {
|
|
15
|
+
resolve(token) {
|
|
16
|
+
return raw.resolve(token);
|
|
17
|
+
},
|
|
18
|
+
register(registrations) {
|
|
19
|
+
const awilixRegs = {};
|
|
20
|
+
for (const [key, value] of Object.entries(registrations)) {
|
|
21
|
+
if (value === null || value === void 0) {
|
|
22
|
+
awilixRegs[key] = asValue(value);
|
|
23
|
+
} else if (typeof value === "function" && value.prototype && Object.getOwnPropertyNames(value.prototype).length > 1) {
|
|
24
|
+
awilixRegs[key] = asClass(value, { lifetime: "SINGLETON" });
|
|
25
|
+
} else if (typeof value === "function") {
|
|
26
|
+
awilixRegs[key] = asFunction(value, { lifetime: "SINGLETON" });
|
|
27
|
+
} else {
|
|
28
|
+
awilixRegs[key] = asValue(value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
raw.register(awilixRegs);
|
|
32
|
+
},
|
|
33
|
+
createScope() {
|
|
34
|
+
return wrapContainer(raw.createScope());
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
return container;
|
|
38
|
+
}
|
|
39
|
+
function createMeridianContainer() {
|
|
40
|
+
const raw = createContainer({
|
|
41
|
+
injectionMode: InjectionMode.PROXY,
|
|
42
|
+
strict: false
|
|
43
|
+
});
|
|
44
|
+
return wrapContainer(raw);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/config-loader.ts
|
|
48
|
+
import path from "path";
|
|
49
|
+
import { pathToFileURL } from "url";
|
|
50
|
+
async function loadConfig(rootDir, configPath) {
|
|
51
|
+
const resolvedPath = configPath ?? path.join(rootDir, "meridian.config.ts");
|
|
52
|
+
const candidates = configPath ? [resolvedPath] : [
|
|
53
|
+
path.join(rootDir, "meridian.config.ts"),
|
|
54
|
+
path.join(rootDir, "meridian.config.mts"),
|
|
55
|
+
path.join(rootDir, "meridian.config.js"),
|
|
56
|
+
path.join(rootDir, "meridian.config.mjs"),
|
|
57
|
+
path.join(rootDir, "meridian.config.cjs")
|
|
58
|
+
];
|
|
59
|
+
let config = null;
|
|
60
|
+
for (const candidate of candidates) {
|
|
61
|
+
try {
|
|
62
|
+
const mod = await import(pathToFileURL(candidate).href);
|
|
63
|
+
const raw = mod.default ?? mod;
|
|
64
|
+
config = raw;
|
|
65
|
+
break;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err.code !== "ERR_MODULE_NOT_FOUND" && err.code !== "MODULE_NOT_FOUND") {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Failed to load Meridian config from "${candidate}": ${err.message}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!config) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Could not find meridian.config.ts in "${rootDir}". Make sure you have a meridian.config.ts file in your project root.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
validateConfig(config);
|
|
80
|
+
return config;
|
|
81
|
+
}
|
|
82
|
+
function validateConfig(config) {
|
|
83
|
+
if (!config.projectConfig) {
|
|
84
|
+
throw new Error("meridian.config: missing required field 'projectConfig'");
|
|
85
|
+
}
|
|
86
|
+
if (!config.projectConfig.databaseUrl) {
|
|
87
|
+
throw new Error("meridian.config.projectConfig: missing required field 'databaseUrl'");
|
|
88
|
+
}
|
|
89
|
+
if (!config.projectConfig.jwtSecret) {
|
|
90
|
+
throw new Error("meridian.config.projectConfig: missing required field 'jwtSecret'");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/module-loader.ts
|
|
95
|
+
import path2 from "path";
|
|
96
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
97
|
+
async function loadModules(globalContainer, moduleConfigs, rootDir) {
|
|
98
|
+
const logger = globalContainer.resolve("logger");
|
|
99
|
+
for (const moduleConfig of moduleConfigs) {
|
|
100
|
+
const definition = await resolveModuleDefinition(moduleConfig, rootDir);
|
|
101
|
+
logger.info(`Loading module: ${definition.key}`);
|
|
102
|
+
const moduleContainer = globalContainer.createScope();
|
|
103
|
+
const moduleOptions = moduleConfig.options ?? {};
|
|
104
|
+
moduleContainer.register({
|
|
105
|
+
moduleOptions,
|
|
106
|
+
logger
|
|
107
|
+
});
|
|
108
|
+
if (definition.loaders) {
|
|
109
|
+
for (const loader of definition.loaders) {
|
|
110
|
+
await loader({
|
|
111
|
+
container: moduleContainer,
|
|
112
|
+
options: moduleConfig.options,
|
|
113
|
+
logger
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const ServiceClass = definition.service;
|
|
118
|
+
const serviceInstance = new ServiceClass(moduleContainer);
|
|
119
|
+
moduleContainer.register({ [definition.key]: serviceInstance });
|
|
120
|
+
globalContainer.register({ [definition.key]: serviceInstance });
|
|
121
|
+
logger.info(`Module loaded: ${definition.key}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function resolveModuleDefinition(config, rootDir) {
|
|
125
|
+
if (typeof config.resolve === "object" && config.resolve !== null) {
|
|
126
|
+
return config.resolve;
|
|
127
|
+
}
|
|
128
|
+
const resolveStr = config.resolve;
|
|
129
|
+
const importPath = resolveStr.startsWith(".") ? pathToFileURL2(path2.resolve(rootDir, resolveStr)).href : resolveStr;
|
|
130
|
+
const mod = await import(importPath);
|
|
131
|
+
const definition = mod.default ?? mod;
|
|
132
|
+
if (!definition?.key || !definition?.service) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Module at "${resolveStr}" must export a ModuleDefinition with 'key' and 'service'. Use the Module() helper from @meridianjs/framework-utils.`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return definition;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/plugin-loader.ts
|
|
141
|
+
import path6 from "path";
|
|
142
|
+
import fs4 from "fs/promises";
|
|
143
|
+
import { pathToFileURL as pathToFileURL6 } from "url";
|
|
144
|
+
import { createRequire } from "module";
|
|
145
|
+
|
|
146
|
+
// src/route-loader.ts
|
|
147
|
+
import fs from "fs/promises";
|
|
148
|
+
import path3 from "path";
|
|
149
|
+
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
150
|
+
import { Router } from "express";
|
|
151
|
+
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
152
|
+
async function loadRoutes(app, container, apiDir) {
|
|
153
|
+
const logger = container.resolve("logger");
|
|
154
|
+
try {
|
|
155
|
+
await fs.access(apiDir);
|
|
156
|
+
} catch {
|
|
157
|
+
logger.debug(`No API directory found at ${apiDir}, skipping route loading.`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const router = Router();
|
|
161
|
+
await scanApiDir(router, apiDir, apiDir, logger);
|
|
162
|
+
app.use("/", router);
|
|
163
|
+
}
|
|
164
|
+
function sortEntriesForRouteOrder(entries) {
|
|
165
|
+
return [...entries].sort((a, b) => {
|
|
166
|
+
const aParam = a.startsWith("[") ? 1 : 0;
|
|
167
|
+
const bParam = b.startsWith("[") ? 1 : 0;
|
|
168
|
+
return aParam - bParam;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async function scanApiDir(router, dir, baseDir, logger) {
|
|
172
|
+
let entries;
|
|
173
|
+
try {
|
|
174
|
+
entries = await fs.readdir(dir);
|
|
175
|
+
} catch {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const sorted = sortEntriesForRouteOrder(entries);
|
|
179
|
+
for (const entry of sorted) {
|
|
180
|
+
const fullPath = path3.join(dir, entry);
|
|
181
|
+
const stat = await fs.stat(fullPath);
|
|
182
|
+
if (stat.isDirectory()) {
|
|
183
|
+
await scanApiDir(router, fullPath, baseDir, logger);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (!/^route\.(ts|mts|js|mjs|cjs)$/.test(entry)) continue;
|
|
187
|
+
const routePath = filePathToRoutePath(fullPath, baseDir);
|
|
188
|
+
let routeModule;
|
|
189
|
+
try {
|
|
190
|
+
routeModule = await import(pathToFileURL3(fullPath).href);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
logger.error(`Failed to load route file ${fullPath}: ${err.message}`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
let registeredCount = 0;
|
|
196
|
+
for (const method of HTTP_METHODS) {
|
|
197
|
+
const handler = routeModule[method];
|
|
198
|
+
if (typeof handler !== "function") continue;
|
|
199
|
+
const expressMethod = method.toLowerCase();
|
|
200
|
+
router[expressMethod](
|
|
201
|
+
routePath,
|
|
202
|
+
async (req, res, next) => {
|
|
203
|
+
try {
|
|
204
|
+
await handler(req, res, next);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
next(err);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
registeredCount++;
|
|
211
|
+
}
|
|
212
|
+
const middlewares = routeModule["middlewares"];
|
|
213
|
+
if (Array.isArray(middlewares)) {
|
|
214
|
+
router.use(routePath, ...middlewares);
|
|
215
|
+
}
|
|
216
|
+
if (registeredCount > 0) {
|
|
217
|
+
logger.debug(`Registered route: ${routePath} [${Object.keys(routeModule).filter((k) => HTTP_METHODS.includes(k)).join(", ")}]`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function filePathToRoutePath(filePath, baseDir) {
|
|
222
|
+
const relative = path3.relative(baseDir, filePath);
|
|
223
|
+
const withoutFile = relative.replace(/[/\\]?route\.(ts|mts|js|mjs|cjs)$/, "");
|
|
224
|
+
const normalized = withoutFile.replace(/\\/g, "/");
|
|
225
|
+
const withParams = normalized.replace(/\[(\w+)\]/g, ":$1");
|
|
226
|
+
return withParams ? `/${withParams}` : "/";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/subscriber-loader.ts
|
|
230
|
+
import fs2 from "fs/promises";
|
|
231
|
+
import path4 from "path";
|
|
232
|
+
import { pathToFileURL as pathToFileURL4 } from "url";
|
|
233
|
+
async function loadSubscribers(container, subscribersDir) {
|
|
234
|
+
const logger = container.resolve("logger");
|
|
235
|
+
try {
|
|
236
|
+
await fs2.access(subscribersDir);
|
|
237
|
+
} catch {
|
|
238
|
+
logger.debug(`No subscribers directory at ${subscribersDir}, skipping.`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const eventBus = container.resolve("eventBus");
|
|
242
|
+
const files = await fs2.readdir(subscribersDir);
|
|
243
|
+
for (const file of files) {
|
|
244
|
+
if (!/\.(ts|mts|js|mjs|cjs)$/.test(file)) continue;
|
|
245
|
+
const fullPath = path4.join(subscribersDir, file);
|
|
246
|
+
let mod;
|
|
247
|
+
try {
|
|
248
|
+
mod = await import(pathToFileURL4(fullPath).href);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
logger.error(`Failed to load subscriber ${file}: ${err.message}`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const handler = mod.default;
|
|
254
|
+
const config = mod.config;
|
|
255
|
+
if (typeof handler !== "function") {
|
|
256
|
+
logger.warn(`Subscriber ${file} is missing a default export function.`);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (!config?.event) {
|
|
260
|
+
logger.warn(`Subscriber ${file} is missing a config.event export.`);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const events = Array.isArray(config.event) ? config.event : [config.event];
|
|
264
|
+
for (const eventName of events) {
|
|
265
|
+
eventBus.subscribe(
|
|
266
|
+
eventName,
|
|
267
|
+
(args) => handler({ ...args, container })
|
|
268
|
+
);
|
|
269
|
+
logger.debug(`Subscriber registered: ${file} \u2192 ${eventName}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/job-loader.ts
|
|
275
|
+
import fs3 from "fs/promises";
|
|
276
|
+
import path5 from "path";
|
|
277
|
+
import { pathToFileURL as pathToFileURL5 } from "url";
|
|
278
|
+
async function loadJobs(container, jobsDir) {
|
|
279
|
+
const logger = container.resolve("logger");
|
|
280
|
+
let scheduler = null;
|
|
281
|
+
try {
|
|
282
|
+
scheduler = container.resolve("scheduler");
|
|
283
|
+
} catch {
|
|
284
|
+
logger.debug("No scheduler registered, skipping job loading.");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
await fs3.access(jobsDir);
|
|
289
|
+
} catch {
|
|
290
|
+
logger.debug(`No jobs directory at ${jobsDir}, skipping.`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const files = await fs3.readdir(jobsDir);
|
|
294
|
+
for (const file of files) {
|
|
295
|
+
if (!/\.(ts|mts|js|mjs|cjs)$/.test(file)) continue;
|
|
296
|
+
const fullPath = path5.join(jobsDir, file);
|
|
297
|
+
let mod;
|
|
298
|
+
try {
|
|
299
|
+
mod = await import(pathToFileURL5(fullPath).href);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
logger.error(`Failed to load job ${file}: ${err.message}`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const fn = mod.default;
|
|
305
|
+
const config = mod.config;
|
|
306
|
+
if (typeof fn !== "function") {
|
|
307
|
+
logger.warn(`Job ${file} is missing a default export function.`);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (!config?.name || !config?.schedule) {
|
|
311
|
+
logger.warn(`Job ${file} is missing a valid config export.`);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
await scheduler.register(config, () => fn(container));
|
|
315
|
+
logger.info(`Scheduled job registered: ${config.name} (${JSON.stringify(config.schedule)})`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/plugin-loader.ts
|
|
320
|
+
async function loadPlugins(container, plugins, rootDir) {
|
|
321
|
+
const logger = container.resolve("logger");
|
|
322
|
+
for (const plugin of plugins) {
|
|
323
|
+
logger.info(`Loading plugin: ${plugin.resolve}`);
|
|
324
|
+
const isLocalPath = plugin.resolve.startsWith(".") || path6.isAbsolute(plugin.resolve);
|
|
325
|
+
const importPath = isLocalPath ? pathToFileURL6(path6.resolve(rootDir, plugin.resolve)).href : plugin.resolve;
|
|
326
|
+
let pluginMod;
|
|
327
|
+
try {
|
|
328
|
+
pluginMod = await import(importPath);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
logger.error(`Failed to load plugin "${plugin.resolve}": ${err.message}`);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
let scanRoot = null;
|
|
334
|
+
if (pluginMod.pluginRoot && typeof pluginMod.pluginRoot === "string") {
|
|
335
|
+
scanRoot = pluginMod.pluginRoot;
|
|
336
|
+
} else if (isLocalPath) {
|
|
337
|
+
const resolvedPath = path6.resolve(rootDir, plugin.resolve);
|
|
338
|
+
try {
|
|
339
|
+
const stat = await fs4.stat(resolvedPath);
|
|
340
|
+
scanRoot = stat.isDirectory() ? resolvedPath : path6.dirname(resolvedPath);
|
|
341
|
+
} catch {
|
|
342
|
+
scanRoot = path6.dirname(resolvedPath);
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
scanRoot = resolveNpmPackageRoot(plugin.resolve, rootDir);
|
|
346
|
+
}
|
|
347
|
+
const ctx = {
|
|
348
|
+
container,
|
|
349
|
+
pluginOptions: plugin.options ?? {},
|
|
350
|
+
addModule: async (moduleConfig) => {
|
|
351
|
+
await loadModules(container, [moduleConfig], scanRoot ?? rootDir);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
if (typeof pluginMod.default === "function") {
|
|
355
|
+
try {
|
|
356
|
+
await pluginMod.default(ctx);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
logger.error(`Plugin register() failed for "${plugin.resolve}": ${err.message}`);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (scanRoot) {
|
|
363
|
+
await autoScanPlugin(scanRoot, container, logger);
|
|
364
|
+
}
|
|
365
|
+
logger.info(`Plugin loaded: ${plugin.resolve}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async function autoScanPlugin(scanRoot, container, logger) {
|
|
369
|
+
const server = container.resolve("server");
|
|
370
|
+
const candidates = [
|
|
371
|
+
scanRoot,
|
|
372
|
+
// pluginRoot already points to compiled dir
|
|
373
|
+
path6.join(scanRoot, "dist"),
|
|
374
|
+
// package root → try dist/ first
|
|
375
|
+
path6.join(scanRoot, "src")
|
|
376
|
+
// package root → try src/ (tsx dev mode)
|
|
377
|
+
];
|
|
378
|
+
for (const candidate of candidates) {
|
|
379
|
+
const apiDir = path6.join(candidate, "api");
|
|
380
|
+
try {
|
|
381
|
+
await fs4.access(apiDir);
|
|
382
|
+
await Promise.all([
|
|
383
|
+
loadRoutes(server, container, apiDir),
|
|
384
|
+
loadSubscribers(container, path6.join(candidate, "subscribers")),
|
|
385
|
+
loadJobs(container, path6.join(candidate, "jobs"))
|
|
386
|
+
]);
|
|
387
|
+
logger.debug(`Plugin auto-scanned from: ${candidate}`);
|
|
388
|
+
return;
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
logger.debug(`No api/ directory found for plugin scan root: ${scanRoot}`);
|
|
393
|
+
}
|
|
394
|
+
function resolveNpmPackageRoot(packageName, fromDir) {
|
|
395
|
+
try {
|
|
396
|
+
const require2 = createRequire(path6.join(fromDir, "synthetic.cjs"));
|
|
397
|
+
const pkgJsonPath = require2.resolve(`${packageName}/package.json`);
|
|
398
|
+
return path6.dirname(pkgJsonPath);
|
|
399
|
+
} catch {
|
|
400
|
+
try {
|
|
401
|
+
const require2 = createRequire(path6.join(fromDir, "synthetic.cjs"));
|
|
402
|
+
const mainPath = require2.resolve(packageName);
|
|
403
|
+
let dir = path6.dirname(mainPath);
|
|
404
|
+
while (dir !== path6.dirname(dir)) {
|
|
405
|
+
try {
|
|
406
|
+
require2.resolve(path6.join(dir, "package.json"));
|
|
407
|
+
return dir;
|
|
408
|
+
} catch {
|
|
409
|
+
dir = path6.dirname(dir);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} catch {
|
|
413
|
+
}
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/link-loader.ts
|
|
419
|
+
import fs5 from "fs/promises";
|
|
420
|
+
import path7 from "path";
|
|
421
|
+
import { pathToFileURL as pathToFileURL7 } from "url";
|
|
422
|
+
async function loadLinks(container, linksDir) {
|
|
423
|
+
const logger = container.resolve("logger");
|
|
424
|
+
const definitions = [];
|
|
425
|
+
try {
|
|
426
|
+
await fs5.access(linksDir);
|
|
427
|
+
} catch {
|
|
428
|
+
logger.debug(`No links directory at ${linksDir}, skipping.`);
|
|
429
|
+
registerEmptyServices(container);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const files = await fs5.readdir(linksDir);
|
|
433
|
+
for (const file of files) {
|
|
434
|
+
if (!/\.(ts|mts|js|mjs|cjs)$/.test(file)) continue;
|
|
435
|
+
const fullPath = path7.join(linksDir, file);
|
|
436
|
+
let mod;
|
|
437
|
+
try {
|
|
438
|
+
mod = await import(pathToFileURL7(fullPath).href);
|
|
439
|
+
} catch (err) {
|
|
440
|
+
logger.error(`Failed to load link file ${file}: ${err.message}`);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const def = mod.default;
|
|
444
|
+
if (def?.linkTableName) {
|
|
445
|
+
definitions.push(def);
|
|
446
|
+
logger.debug(`Link loaded: ${def.linkTableName}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const linkService = new LinkService(definitions, container, logger);
|
|
450
|
+
const queryService = new QueryService(definitions, container, logger);
|
|
451
|
+
container.register({
|
|
452
|
+
link: linkService,
|
|
453
|
+
query: queryService
|
|
454
|
+
});
|
|
455
|
+
logger.info(`Loaded ${definitions.length} module link(s)`);
|
|
456
|
+
}
|
|
457
|
+
function registerEmptyServices(container) {
|
|
458
|
+
const link = new LinkService([], container, null);
|
|
459
|
+
const query = new QueryService([], container, null);
|
|
460
|
+
container.register({ link, query });
|
|
461
|
+
}
|
|
462
|
+
var LinkService = class {
|
|
463
|
+
constructor(defs, container, logger) {
|
|
464
|
+
this.defs = defs;
|
|
465
|
+
this.container = container;
|
|
466
|
+
this.logger = logger;
|
|
467
|
+
this.defsByTable = new Map(defs.map((d) => [d.linkTableName, d]));
|
|
468
|
+
}
|
|
469
|
+
defsByTable;
|
|
470
|
+
async create(linkTableName, leftId, rightId, data) {
|
|
471
|
+
this.logger?.debug(`Link.create: ${linkTableName} ${leftId} \u2192 ${rightId}`);
|
|
472
|
+
}
|
|
473
|
+
async dismiss(linkTableName, leftId, rightId) {
|
|
474
|
+
this.logger?.debug(`Link.dismiss: ${linkTableName} ${leftId} \u2192 ${rightId}`);
|
|
475
|
+
}
|
|
476
|
+
getDefinitions() {
|
|
477
|
+
return this.defs;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
var QueryService = class {
|
|
481
|
+
constructor(defs, container, logger) {
|
|
482
|
+
this.defs = defs;
|
|
483
|
+
this.container = container;
|
|
484
|
+
this.logger = logger;
|
|
485
|
+
}
|
|
486
|
+
async graph(options) {
|
|
487
|
+
this.logger?.debug(`Query.graph: ${options.entity}`, { fields: options.fields });
|
|
488
|
+
return { data: [] };
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// src/server.ts
|
|
493
|
+
import express from "express";
|
|
494
|
+
import cors from "cors";
|
|
495
|
+
import helmet from "helmet";
|
|
496
|
+
function createServer(container, config) {
|
|
497
|
+
const app = express();
|
|
498
|
+
const logger = container.resolve("logger");
|
|
499
|
+
app.use(express.json({ limit: "10mb" }));
|
|
500
|
+
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
|
|
501
|
+
app.use(helmet({
|
|
502
|
+
contentSecurityPolicy: {
|
|
503
|
+
directives: {
|
|
504
|
+
defaultSrc: ["'self'"],
|
|
505
|
+
scriptSrc: ["'self'"],
|
|
506
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
507
|
+
imgSrc: ["'self'", "data:", "blob:"],
|
|
508
|
+
connectSrc: ["'self'"]
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
crossOriginEmbedderPolicy: false
|
|
512
|
+
}));
|
|
513
|
+
const corsOrigin = config.projectConfig.cors?.origin ?? "*";
|
|
514
|
+
if (corsOrigin === "*" && process.env.NODE_ENV === "production") {
|
|
515
|
+
logger.warn(
|
|
516
|
+
"CORS origin is set to '*' in production. Set projectConfig.cors.origin to a specific domain to restrict cross-origin access."
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
app.use(
|
|
520
|
+
cors({
|
|
521
|
+
origin: corsOrigin,
|
|
522
|
+
credentials: config.projectConfig.cors?.credentials ?? false,
|
|
523
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
524
|
+
allowedHeaders: ["Content-Type", "Authorization"],
|
|
525
|
+
exposedHeaders: ["X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset"]
|
|
526
|
+
})
|
|
527
|
+
);
|
|
528
|
+
app.use((req, _res, next) => {
|
|
529
|
+
req.scope = container.createScope();
|
|
530
|
+
next();
|
|
531
|
+
});
|
|
532
|
+
app.get("/health", (_req, res) => {
|
|
533
|
+
res.json({ ok: true, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
534
|
+
});
|
|
535
|
+
app.get("/ready", (_req, res) => {
|
|
536
|
+
res.json({ ok: true });
|
|
537
|
+
});
|
|
538
|
+
app.use((err, _req, res, _next) => {
|
|
539
|
+
const status = err.status ?? err.statusCode ?? 500;
|
|
540
|
+
const message = err.message ?? "Internal Server Error";
|
|
541
|
+
if (status >= 500) {
|
|
542
|
+
logger.error(`Unhandled error: ${message}`, {
|
|
543
|
+
stack: err.stack,
|
|
544
|
+
status
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
res.status(status).json({
|
|
548
|
+
error: {
|
|
549
|
+
message,
|
|
550
|
+
type: err.type ?? err.name ?? "Error",
|
|
551
|
+
...process.env.NODE_ENV === "development" && { stack: err.stack }
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
return app;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/logger.ts
|
|
559
|
+
var ConsoleLogger = class {
|
|
560
|
+
constructor(prefix = "meridian") {
|
|
561
|
+
this.prefix = prefix;
|
|
562
|
+
}
|
|
563
|
+
format(level, message, meta) {
|
|
564
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
565
|
+
const base = `[${ts}] [${level.toUpperCase()}] [${this.prefix}] ${message}`;
|
|
566
|
+
return meta ? `${base} ${JSON.stringify(meta)}` : base;
|
|
567
|
+
}
|
|
568
|
+
info(message, meta) {
|
|
569
|
+
console.log(this.format("info", message, meta));
|
|
570
|
+
}
|
|
571
|
+
warn(message, meta) {
|
|
572
|
+
console.warn(this.format("warn", message, meta));
|
|
573
|
+
}
|
|
574
|
+
error(message, meta) {
|
|
575
|
+
console.error(this.format("error", message, meta));
|
|
576
|
+
}
|
|
577
|
+
debug(message, meta) {
|
|
578
|
+
if (process.env.DEBUG) {
|
|
579
|
+
console.debug(this.format("debug", message, meta));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// src/bootstrap.ts
|
|
585
|
+
async function bootstrap(opts) {
|
|
586
|
+
const { rootDir } = opts;
|
|
587
|
+
const config = await loadConfig(rootDir, opts.configPath);
|
|
588
|
+
const container = createMeridianContainer();
|
|
589
|
+
const logger = new ConsoleLogger("meridian");
|
|
590
|
+
container.register({
|
|
591
|
+
logger,
|
|
592
|
+
config
|
|
593
|
+
});
|
|
594
|
+
logger.info("Bootstrapping Meridian application...");
|
|
595
|
+
await loadModules(container, config.modules ?? [], rootDir);
|
|
596
|
+
let eventBus;
|
|
597
|
+
try {
|
|
598
|
+
eventBus = container.resolve("eventBus");
|
|
599
|
+
} catch {
|
|
600
|
+
logger.warn(
|
|
601
|
+
"No eventBus module registered. Events will not be dispatched. Add @meridianjs/event-bus-local or @meridianjs/event-bus-redis to your config.modules."
|
|
602
|
+
);
|
|
603
|
+
eventBus = {
|
|
604
|
+
async emit() {
|
|
605
|
+
},
|
|
606
|
+
subscribe() {
|
|
607
|
+
},
|
|
608
|
+
unsubscribe() {
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
container.register({ eventBus });
|
|
612
|
+
}
|
|
613
|
+
await loadPlugins(container, config.plugins ?? [], rootDir);
|
|
614
|
+
await loadLinks(container, path8.join(rootDir, "src", "links"));
|
|
615
|
+
const server = createServer(container, config);
|
|
616
|
+
container.register({ server });
|
|
617
|
+
await loadMiddlewares(server, container, path8.join(rootDir, "src", "api"));
|
|
618
|
+
await loadRoutes(server, container, path8.join(rootDir, "src", "api"));
|
|
619
|
+
await loadSubscribers(container, path8.join(rootDir, "src", "subscribers"));
|
|
620
|
+
await loadJobs(container, path8.join(rootDir, "src", "jobs"));
|
|
621
|
+
const httpServer = http.createServer(server);
|
|
622
|
+
logger.info("Meridian application bootstrapped successfully.");
|
|
623
|
+
return {
|
|
624
|
+
container,
|
|
625
|
+
server,
|
|
626
|
+
httpServer,
|
|
627
|
+
async start() {
|
|
628
|
+
const port = config.projectConfig.httpPort ?? 9e3;
|
|
629
|
+
await new Promise((resolve, reject) => {
|
|
630
|
+
httpServer.listen(port, () => {
|
|
631
|
+
logger.info(`Meridian server running on http://localhost:${port}`);
|
|
632
|
+
resolve();
|
|
633
|
+
});
|
|
634
|
+
httpServer.on("error", reject);
|
|
635
|
+
});
|
|
636
|
+
},
|
|
637
|
+
async stop() {
|
|
638
|
+
logger.info("Shutting down Meridian server...");
|
|
639
|
+
await new Promise((resolve, reject) => {
|
|
640
|
+
httpServer.close((err) => err ? reject(err) : resolve());
|
|
641
|
+
});
|
|
642
|
+
try {
|
|
643
|
+
const bus = container.resolve("eventBus");
|
|
644
|
+
await bus.close?.();
|
|
645
|
+
} catch {
|
|
646
|
+
}
|
|
647
|
+
logger.info("Meridian server stopped.");
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
async function loadMiddlewares(server, container, apiDir) {
|
|
652
|
+
const logger = container.resolve("logger");
|
|
653
|
+
const middlewaresPath = path8.join(apiDir, "middlewares.ts");
|
|
654
|
+
let mod;
|
|
655
|
+
try {
|
|
656
|
+
const { pathToFileURL: pathToFileURL8 } = await import("url");
|
|
657
|
+
mod = await import(pathToFileURL8(middlewaresPath).href);
|
|
658
|
+
} catch {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const config = mod.default;
|
|
662
|
+
if (!config?.routes) return;
|
|
663
|
+
for (const route of config.routes) {
|
|
664
|
+
server.use(route.matcher, ...route.middlewares);
|
|
665
|
+
logger.debug(`Middleware applied: ${route.matcher}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// src/utils/define-config.ts
|
|
670
|
+
function defineConfig(config) {
|
|
671
|
+
return config;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/utils/define-middlewares.ts
|
|
675
|
+
function defineMiddlewares(config) {
|
|
676
|
+
return config;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/rate-limit.ts
|
|
680
|
+
import rateLimit from "express-rate-limit";
|
|
681
|
+
var sharedOpts = {
|
|
682
|
+
standardHeaders: true,
|
|
683
|
+
legacyHeaders: false,
|
|
684
|
+
message: { error: { message: "Too many requests" } }
|
|
685
|
+
};
|
|
686
|
+
var authRateLimit = rateLimit({
|
|
687
|
+
windowMs: 6e4,
|
|
688
|
+
max: 10,
|
|
689
|
+
...sharedOpts
|
|
690
|
+
});
|
|
691
|
+
var apiRateLimit = rateLimit({
|
|
692
|
+
windowMs: 6e4,
|
|
693
|
+
max: 300,
|
|
694
|
+
...sharedOpts
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// src/validate.ts
|
|
698
|
+
function validate(schema) {
|
|
699
|
+
return (req, res, next) => {
|
|
700
|
+
const result = schema.safeParse(req.body);
|
|
701
|
+
if (!result.success) {
|
|
702
|
+
res.status(400).json({
|
|
703
|
+
error: {
|
|
704
|
+
message: "Validation error",
|
|
705
|
+
details: result.error.flatten().fieldErrors
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
req.body = result.data;
|
|
711
|
+
next();
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
export {
|
|
715
|
+
ConsoleLogger,
|
|
716
|
+
apiRateLimit,
|
|
717
|
+
asClass,
|
|
718
|
+
asFunction,
|
|
719
|
+
asValue,
|
|
720
|
+
authRateLimit,
|
|
721
|
+
bootstrap,
|
|
722
|
+
createMeridianContainer,
|
|
723
|
+
createServer,
|
|
724
|
+
defineConfig,
|
|
725
|
+
defineMiddlewares,
|
|
726
|
+
loadConfig,
|
|
727
|
+
loadJobs,
|
|
728
|
+
loadLinks,
|
|
729
|
+
loadModules,
|
|
730
|
+
loadPlugins,
|
|
731
|
+
loadRoutes,
|
|
732
|
+
loadSubscribers,
|
|
733
|
+
resolveModuleDefinition,
|
|
734
|
+
validate
|
|
735
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meridianjs/framework",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Core Meridian framework: bootstrap, DI container, module/route/subscriber/job loaders",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
17
|
+
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@meridianjs/framework-utils": "^0.1.0",
|
|
25
|
+
"@meridianjs/types": "^0.1.0",
|
|
26
|
+
"awilix": "^12.0.5",
|
|
27
|
+
"cors": "^2.8.5",
|
|
28
|
+
"express": "^4.21.2",
|
|
29
|
+
"express-rate-limit": "^7.5.0",
|
|
30
|
+
"helmet": "^8.0.0",
|
|
31
|
+
"zod": "^3.24.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/cors": "^2.8.17",
|
|
35
|
+
"@types/express": "^5.0.0",
|
|
36
|
+
"tsup": "^8.3.5",
|
|
37
|
+
"tsx": "4.21.0",
|
|
38
|
+
"typescript": "*",
|
|
39
|
+
"vitest": "*"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist"
|
|
43
|
+
],
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
}
|
|
47
|
+
}
|