arcway 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/LICENSE +21 -0
- package/README.md +711 -0
- package/client/env.js +55 -0
- package/client/fetcher.js +50 -0
- package/client/graphql.js +35 -0
- package/client/head.js +140 -0
- package/client/hooks/use-api.js +80 -0
- package/client/hooks/use-debounce.js +12 -0
- package/client/hooks/use-form.js +86 -0
- package/client/hooks/use-graphql.js +30 -0
- package/client/hooks/use-interval.js +12 -0
- package/client/hooks/use-mutation.js +27 -0
- package/client/hooks/use-query.js +45 -0
- package/client/hooks/web/use-click-outside.js +22 -0
- package/client/hooks/web/use-local-storage.js +42 -0
- package/client/index.js +62 -0
- package/client/page-loader.js +155 -0
- package/client/provider.js +53 -0
- package/client/query.js +13 -0
- package/client/router.jsx +303 -0
- package/client/ui/accordion.jsx +65 -0
- package/client/ui/accordion.stories.jsx +48 -0
- package/client/ui/alert-dialog.jsx +122 -0
- package/client/ui/alert-dialog.stories.jsx +44 -0
- package/client/ui/alert.jsx +52 -0
- package/client/ui/alert.stories.jsx +31 -0
- package/client/ui/app-shell.jsx +39 -0
- package/client/ui/app-shell.stories.jsx +51 -0
- package/client/ui/aspect-ratio.jsx +6 -0
- package/client/ui/aspect-ratio.stories.jsx +69 -0
- package/client/ui/avatar.jsx +78 -0
- package/client/ui/avatar.stories.jsx +62 -0
- package/client/ui/badge.jsx +34 -0
- package/client/ui/badge.stories.js +32 -0
- package/client/ui/breadcrumb.jsx +86 -0
- package/client/ui/breadcrumb.stories.jsx +43 -0
- package/client/ui/button-group.jsx +58 -0
- package/client/ui/button-group.stories.jsx +67 -0
- package/client/ui/button.jsx +46 -0
- package/client/ui/button.stories.js +72 -0
- package/client/ui/calendar.jsx +172 -0
- package/client/ui/card.jsx +57 -0
- package/client/ui/card.stories.jsx +33 -0
- package/client/ui/carousel.jsx +167 -0
- package/client/ui/chart.jsx +244 -0
- package/client/ui/checkbox.jsx +24 -0
- package/client/ui/checkbox.stories.js +33 -0
- package/client/ui/collapsible.jsx +12 -0
- package/client/ui/collapsible.stories.jsx +42 -0
- package/client/ui/combobox.jsx +223 -0
- package/client/ui/command.jsx +128 -0
- package/client/ui/context-menu.jsx +170 -0
- package/client/ui/context-menu.stories.jsx +35 -0
- package/client/ui/dialog.jsx +109 -0
- package/client/ui/dialog.stories.jsx +37 -0
- package/client/ui/direction.jsx +9 -0
- package/client/ui/drawer.jsx +87 -0
- package/client/ui/dropdown-menu.jsx +172 -0
- package/client/ui/dropdown-menu.stories.jsx +34 -0
- package/client/ui/empty.jsx +76 -0
- package/client/ui/empty.stories.jsx +64 -0
- package/client/ui/field.jsx +174 -0
- package/client/ui/field.stories.jsx +118 -0
- package/client/ui/form.jsx +17 -0
- package/client/ui/hooks/use-mobile.js +16 -0
- package/client/ui/hover-card.jsx +26 -0
- package/client/ui/hover-card.stories.jsx +28 -0
- package/client/ui/index.js +649 -0
- package/client/ui/input-group.jsx +116 -0
- package/client/ui/input-group.stories.jsx +65 -0
- package/client/ui/input-otp.jsx +62 -0
- package/client/ui/input.jsx +16 -0
- package/client/ui/input.stories.js +27 -0
- package/client/ui/item.jsx +155 -0
- package/client/ui/item.stories.jsx +118 -0
- package/client/ui/kbd.jsx +24 -0
- package/client/ui/kbd.stories.jsx +32 -0
- package/client/ui/label.jsx +16 -0
- package/client/ui/label.stories.js +25 -0
- package/client/ui/lib/utils.js +6 -0
- package/client/ui/main-content.jsx +30 -0
- package/client/ui/menubar.jsx +189 -0
- package/client/ui/menubar.stories.jsx +43 -0
- package/client/ui/native-select.jsx +34 -0
- package/client/ui/native-select.stories.jsx +67 -0
- package/client/ui/navigation-menu.jsx +120 -0
- package/client/ui/navigation-menu.stories.jsx +45 -0
- package/client/ui/pagination.jsx +92 -0
- package/client/ui/pagination.stories.jsx +52 -0
- package/client/ui/panel.jsx +66 -0
- package/client/ui/popover.jsx +54 -0
- package/client/ui/popover.stories.jsx +27 -0
- package/client/ui/progress.jsx +19 -0
- package/client/ui/progress.stories.js +34 -0
- package/client/ui/radio-group.jsx +33 -0
- package/client/ui/radio-group.stories.jsx +49 -0
- package/client/ui/resizable.jsx +33 -0
- package/client/ui/scroll-area.jsx +41 -0
- package/client/ui/scroll-area.stories.jsx +43 -0
- package/client/ui/select.jsx +145 -0
- package/client/ui/select.stories.jsx +80 -0
- package/client/ui/separator.jsx +18 -0
- package/client/ui/separator.stories.jsx +37 -0
- package/client/ui/sheet.jsx +95 -0
- package/client/ui/sheet.stories.jsx +56 -0
- package/client/ui/sidebar.jsx +544 -0
- package/client/ui/skeleton.jsx +8 -0
- package/client/ui/skeleton.stories.js +23 -0
- package/client/ui/slider.jsx +41 -0
- package/client/ui/slider.stories.js +31 -0
- package/client/ui/sonner.jsx +37 -0
- package/client/ui/spinner.jsx +14 -0
- package/client/ui/spinner.stories.js +16 -0
- package/client/ui/style-mira.css +1316 -0
- package/client/ui/switch.jsx +22 -0
- package/client/ui/switch.stories.js +44 -0
- package/client/ui/table.jsx +33 -0
- package/client/ui/table.stories.jsx +42 -0
- package/client/ui/tabs.jsx +63 -0
- package/client/ui/tabs.stories.jsx +45 -0
- package/client/ui/textarea.jsx +15 -0
- package/client/ui/textarea.stories.js +33 -0
- package/client/ui/theme.css +459 -0
- package/client/ui/toggle-group.jsx +62 -0
- package/client/ui/toggle-group.stories.jsx +68 -0
- package/client/ui/toggle.jsx +34 -0
- package/client/ui/toggle.stories.js +46 -0
- package/client/ui/tooltip.jsx +37 -0
- package/client/ui/tooltip.stories.jsx +32 -0
- package/client/ui/use-transition.js +35 -0
- package/client/ws.js +132 -0
- package/package.json +134 -0
- package/server/bin/cli.js +42 -0
- package/server/bin/commands/build.js +23 -0
- package/server/bin/commands/dev.js +57 -0
- package/server/bin/commands/docs.js +30 -0
- package/server/bin/commands/graphql-schema.js +32 -0
- package/server/bin/commands/lint.js +35 -0
- package/server/bin/commands/mcp.js +26 -0
- package/server/bin/commands/migrate.js +82 -0
- package/server/bin/commands/schema.js +41 -0
- package/server/bin/commands/seed.js +36 -0
- package/server/bin/commands/start.js +31 -0
- package/server/bin/commands/test.js +20 -0
- package/server/bin/solo.js +4 -0
- package/server/boot/index.js +150 -0
- package/server/boot.js +2 -0
- package/server/build.js +23 -0
- package/server/cache/drivers/memory.js +23 -0
- package/server/cache/drivers/redis.js +28 -0
- package/server/cache/index.js +69 -0
- package/server/config/loader.js +89 -0
- package/server/config/modules/api.js +17 -0
- package/server/config/modules/build.js +9 -0
- package/server/config/modules/cache.js +10 -0
- package/server/config/modules/database.js +29 -0
- package/server/config/modules/events.js +15 -0
- package/server/config/modules/files.js +15 -0
- package/server/config/modules/jobs.js +20 -0
- package/server/config/modules/logger.js +9 -0
- package/server/config/modules/mail.js +11 -0
- package/server/config/modules/mcp.js +9 -0
- package/server/config/modules/pages.js +20 -0
- package/server/config/modules/queue.js +10 -0
- package/server/config/modules/redis.js +9 -0
- package/server/config/modules/server.js +30 -0
- package/server/config/modules/session.js +9 -0
- package/server/config/modules/websocket.js +11 -0
- package/server/constants.js +67 -0
- package/server/context.js +15 -0
- package/server/db/index.js +87 -0
- package/server/db/schema/drivers/mysql.js +28 -0
- package/server/db/schema/drivers/pg.js +34 -0
- package/server/db/schema/drivers/sqlite.js +22 -0
- package/server/db/schema/index.js +78 -0
- package/server/db/seeds.js +22 -0
- package/server/discovery.js +67 -0
- package/server/docs/openapi.js +153 -0
- package/server/env.js +17 -0
- package/server/events/drivers/memory.js +45 -0
- package/server/events/drivers/redis.js +64 -0
- package/server/events/handler.js +67 -0
- package/server/events/index.js +35 -0
- package/server/events/pattern.js +5 -0
- package/server/files/drivers/local.js +83 -0
- package/server/files/drivers/s3.js +113 -0
- package/server/files/index.js +57 -0
- package/server/filewatcher/index.js +156 -0
- package/server/glob.js +6 -0
- package/server/graphql/discovery.js +70 -0
- package/server/graphql/handler.js +41 -0
- package/server/graphql/index.js +13 -0
- package/server/graphql/loaders.js +19 -0
- package/server/graphql/merge.js +48 -0
- package/server/graphql/subscriptions.js +43 -0
- package/server/health.js +34 -0
- package/server/helpers.js +9 -0
- package/server/index.js +55 -0
- package/server/internals.js +139 -0
- package/server/jobs/cron.js +10 -0
- package/server/jobs/drivers/knex-queue.js +207 -0
- package/server/jobs/drivers/lease.js +148 -0
- package/server/jobs/drivers/memory-queue.js +134 -0
- package/server/jobs/queue.js +27 -0
- package/server/jobs/runner.js +197 -0
- package/server/jobs/throughput.js +63 -0
- package/server/lib/vault/encrypt.js +40 -0
- package/server/lib/vault/ids.js +9 -0
- package/server/lib/vault/index.js +14 -0
- package/server/lib/vault/jwt.js +55 -0
- package/server/lib/vault/password.js +10 -0
- package/server/lint/boundaries.js +77 -0
- package/server/logger/index.js +130 -0
- package/server/mail/drivers/console.js +31 -0
- package/server/mail/drivers/smtp.js +34 -0
- package/server/mail/imap.js +105 -0
- package/server/mail/inbound-store.js +58 -0
- package/server/mail/inbound.js +79 -0
- package/server/mail/index.js +112 -0
- package/server/mcp/debug-api.js +137 -0
- package/server/mcp/helpers.js +30 -0
- package/server/mcp/index.js +77 -0
- package/server/mcp/runtime.js +7 -0
- package/server/mcp/server.js +19 -0
- package/server/mcp/tools/debugging.js +133 -0
- package/server/mcp/tools/introspection.js +87 -0
- package/server/middlewares/cors.js +30 -0
- package/server/middlewares/index.js +3 -0
- package/server/middlewares/require-session.js +15 -0
- package/server/module-loader.js +9 -0
- package/server/pages/build-client.js +187 -0
- package/server/pages/build-css.js +47 -0
- package/server/pages/build-manifest.js +55 -0
- package/server/pages/build-plugins.js +75 -0
- package/server/pages/build-server.js +115 -0
- package/server/pages/build.js +116 -0
- package/server/pages/discovery.js +120 -0
- package/server/pages/fonts.js +128 -0
- package/server/pages/handler.js +276 -0
- package/server/pages/hmr.js +176 -0
- package/server/pages/pages-router.js +78 -0
- package/server/pages/ssr.js +276 -0
- package/server/pages/static.js +92 -0
- package/server/pages/watcher.js +90 -0
- package/server/queue/drivers/knex.js +67 -0
- package/server/queue/drivers/redis.js +91 -0
- package/server/queue/index.js +61 -0
- package/server/rate-limit/consume.js +21 -0
- package/server/rate-limit/drivers/memory.js +24 -0
- package/server/rate-limit/drivers/redis.js +32 -0
- package/server/rate-limit/index.js +33 -0
- package/server/redis/index.js +67 -0
- package/server/ring-buffer.js +44 -0
- package/server/route.js +4 -0
- package/server/router/api-router.js +317 -0
- package/server/router/cors.js +31 -0
- package/server/router/middleware.js +91 -0
- package/server/router/routes.js +132 -0
- package/server/server.js +35 -0
- package/server/session/helpers.js +21 -0
- package/server/session/index.js +89 -0
- package/server/static/index.js +36 -0
- package/server/system-jobs/index.js +50 -0
- package/server/system-routes/index.js +84 -0
- package/server/testing/index.js +263 -0
- package/server/validation.js +41 -0
- package/server/watcher.js +34 -0
- package/server/web-server.js +231 -0
- package/server/ws/discovery.js +54 -0
- package/server/ws/index.js +14 -0
- package/server/ws/realtime.js +318 -0
- package/server/ws/registry.js +17 -0
- package/server/ws/server.js +152 -0
- package/server/ws/ws-router.js +335 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
enabled: true,
|
|
5
|
+
dir: 'pages',
|
|
6
|
+
outDir: '.build/pages',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function resolve(config, { rootDir } = {}) {
|
|
10
|
+
const pages = { ...DEFAULTS, ...config.pages };
|
|
11
|
+
if (pages.dir && !path.isAbsolute(pages.dir)) {
|
|
12
|
+
pages.dir = path.resolve(rootDir, pages.dir);
|
|
13
|
+
}
|
|
14
|
+
if (pages.outDir && !path.isAbsolute(pages.outDir)) {
|
|
15
|
+
pages.outDir = path.resolve(rootDir, pages.outDir);
|
|
16
|
+
}
|
|
17
|
+
return { ...config, pages };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default resolve;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const DEFAULTS = {
|
|
2
|
+
host: '0.0.0.0',
|
|
3
|
+
port: 3000,
|
|
4
|
+
maxBodySize: 1024 * 1024,
|
|
5
|
+
shutdownTimeoutMs: 10000,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CORS = {
|
|
9
|
+
origin: '*',
|
|
10
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
11
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
12
|
+
exposedHeaders: [],
|
|
13
|
+
credentials: false,
|
|
14
|
+
maxAge: 86400,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function resolveCors(config, mode) {
|
|
18
|
+
if (config === false) return void 0;
|
|
19
|
+
if (config === void 0 && mode === 'production') return void 0;
|
|
20
|
+
const userConfig = typeof config === 'object' ? config : {};
|
|
21
|
+
return { ...DEFAULT_CORS, ...userConfig };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolve(config, { mode } = {}) {
|
|
25
|
+
const server = { ...DEFAULTS, ...config.server };
|
|
26
|
+
server.cors = resolveCors(server.cors, mode);
|
|
27
|
+
return { ...config, server };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default resolve;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { resolveSessionConfig } from '../../session/index.js';
|
|
2
|
+
|
|
3
|
+
function resolve(config, { mode } = {}) {
|
|
4
|
+
if (!config.session) return config;
|
|
5
|
+
const session = resolveSessionConfig(config.session, mode);
|
|
6
|
+
return { ...config, session };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default resolve;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import Module from 'node:module';
|
|
2
|
+
|
|
3
|
+
const REDIS_SCAN_COUNT = 100;
|
|
4
|
+
const LEASE_HEARTBEAT_INTERVAL_MS = 1e4;
|
|
5
|
+
const LEASE_EXPIRY_MS = 3e4;
|
|
6
|
+
const SESSION_COOKIE_MAX_AGE_SKEW = 60;
|
|
7
|
+
|
|
8
|
+
const NODE_BUILTINS = new Set(
|
|
9
|
+
Module.builtinModules.flatMap((m) => (m.startsWith('node:') ? [m] : [m, `node:${m}`])),
|
|
10
|
+
);
|
|
11
|
+
const SERVER_ONLY_PACKAGES = new Set([
|
|
12
|
+
'knex',
|
|
13
|
+
'better-sqlite3',
|
|
14
|
+
'pg',
|
|
15
|
+
'pg-native',
|
|
16
|
+
'pg-query-stream',
|
|
17
|
+
'tedious',
|
|
18
|
+
'mysql',
|
|
19
|
+
'mysql2',
|
|
20
|
+
'oracledb',
|
|
21
|
+
'sqlite3',
|
|
22
|
+
'pino',
|
|
23
|
+
'pino-pretty',
|
|
24
|
+
'ws',
|
|
25
|
+
'graphql-yoga',
|
|
26
|
+
'graphql-ws',
|
|
27
|
+
'@graphql-tools/schema',
|
|
28
|
+
'@graphql-tools/merge',
|
|
29
|
+
'ioredis',
|
|
30
|
+
'iron-session',
|
|
31
|
+
'bcryptjs',
|
|
32
|
+
'dotenv',
|
|
33
|
+
'chokidar',
|
|
34
|
+
'commander',
|
|
35
|
+
'glob',
|
|
36
|
+
'esbuild',
|
|
37
|
+
'postcss',
|
|
38
|
+
'@tailwindcss/postcss',
|
|
39
|
+
'tailwindcss',
|
|
40
|
+
'@tailwindcss/oxide',
|
|
41
|
+
'tsx',
|
|
42
|
+
'cache-manager',
|
|
43
|
+
'rate-limiter-flexible',
|
|
44
|
+
'cron-parser',
|
|
45
|
+
'@aws-sdk/client-s3',
|
|
46
|
+
'@modelcontextprotocol/sdk',
|
|
47
|
+
'dataloader',
|
|
48
|
+
'cookie',
|
|
49
|
+
]);
|
|
50
|
+
const ErrorCodes = {
|
|
51
|
+
BODY_TOO_LARGE: 'BODY_TOO_LARGE',
|
|
52
|
+
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
|
53
|
+
NOT_FOUND: 'NOT_FOUND',
|
|
54
|
+
INVALID_JSON: 'INVALID_JSON',
|
|
55
|
+
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
56
|
+
HANDLER_ERROR: 'HANDLER_ERROR',
|
|
57
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
58
|
+
};
|
|
59
|
+
export {
|
|
60
|
+
ErrorCodes,
|
|
61
|
+
LEASE_EXPIRY_MS,
|
|
62
|
+
LEASE_HEARTBEAT_INTERVAL_MS,
|
|
63
|
+
NODE_BUILTINS,
|
|
64
|
+
REDIS_SCAN_COUNT,
|
|
65
|
+
SERVER_ONLY_PACKAGES,
|
|
66
|
+
SESSION_COOKIE_MAX_AGE_SKEW,
|
|
67
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function buildContext(appContext, extras) {
|
|
2
|
+
const ctx = {
|
|
3
|
+
db: appContext.db,
|
|
4
|
+
log: appContext.log,
|
|
5
|
+
events: appContext.events,
|
|
6
|
+
cache: appContext.cache,
|
|
7
|
+
queue: appContext.queue,
|
|
8
|
+
files: appContext.files,
|
|
9
|
+
mail: appContext.mail,
|
|
10
|
+
...extras,
|
|
11
|
+
};
|
|
12
|
+
return ctx;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { buildContext };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fsSync from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import knex from 'knex';
|
|
4
|
+
import { discoverFiles } from '../discovery.js';
|
|
5
|
+
import { loadModule } from '../module-loader.js';
|
|
6
|
+
import { introspectSchema, generateSchemaMarkdown } from './schema/index.js';
|
|
7
|
+
|
|
8
|
+
class MigrationSource {
|
|
9
|
+
constructor(directory) {
|
|
10
|
+
this.directory = directory;
|
|
11
|
+
}
|
|
12
|
+
async getMigrations() {
|
|
13
|
+
const files = await discoverFiles(this.directory, { skipUnderscore: false });
|
|
14
|
+
return files.map((f) => f.name);
|
|
15
|
+
}
|
|
16
|
+
getMigrationName(migration) {
|
|
17
|
+
return migration;
|
|
18
|
+
}
|
|
19
|
+
async getMigration(migration) {
|
|
20
|
+
const filePath = path.resolve(this.directory, migration);
|
|
21
|
+
return loadModule(filePath, import.meta.url);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function createDB(config, { log } = {}) {
|
|
26
|
+
const client = config.client;
|
|
27
|
+
const isSqlite = client === 'better-sqlite3' || client === 'sqlite3';
|
|
28
|
+
const connection = config.connection;
|
|
29
|
+
|
|
30
|
+
if (isSqlite && connection?.filename) {
|
|
31
|
+
fsSync.mkdirSync(path.dirname(connection.filename), { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const db = knex({
|
|
35
|
+
client,
|
|
36
|
+
connection,
|
|
37
|
+
...(isSqlite ? { useNullAsDefault: true } : {}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await db.raw('SELECT 1');
|
|
42
|
+
log?.info('Database connected');
|
|
43
|
+
} catch (err) {
|
|
44
|
+
await db.destroy();
|
|
45
|
+
throw new Error(`Database connection failed: ${err}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const origDestroy = db.destroy.bind(db);
|
|
49
|
+
Object.defineProperty(db, 'destroy', {
|
|
50
|
+
value: async () => {
|
|
51
|
+
await origDestroy();
|
|
52
|
+
log?.info('Database connection closed');
|
|
53
|
+
},
|
|
54
|
+
writable: true,
|
|
55
|
+
configurable: true,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
db.runMigrations = async () => {
|
|
59
|
+
const migrationsDir = config.migrationsDir;
|
|
60
|
+
if (!migrationsDir) return;
|
|
61
|
+
const [batch, migrations] = await db.migrate.latest({
|
|
62
|
+
migrationSource: new MigrationSource(migrationsDir),
|
|
63
|
+
});
|
|
64
|
+
if (migrations.length > 0) {
|
|
65
|
+
log?.info(`Migrations (batch ${batch}): ${migrations.join(', ')}`);
|
|
66
|
+
} else {
|
|
67
|
+
log?.info('Migrations: up to date');
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
db.runRollback = async () => {
|
|
72
|
+
const migrationsDir = config.migrationsDir;
|
|
73
|
+
if (!migrationsDir) throw new Error('No migrationsDir configured');
|
|
74
|
+
const [batch, entries] = await db.migrate.rollback({
|
|
75
|
+
migrationSource: new MigrationSource(migrationsDir),
|
|
76
|
+
});
|
|
77
|
+
return { batch, log: entries };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
db.introspectSchema = () => introspectSchema(db);
|
|
81
|
+
|
|
82
|
+
db.generateSchemaMarkdown = generateSchemaMarkdown;
|
|
83
|
+
|
|
84
|
+
return db;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { createDB, MigrationSource };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const EXCLUDED = new Set(['knex_migrations', 'knex_migrations_lock']);
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
clients: ['mysql2'],
|
|
5
|
+
|
|
6
|
+
async tables(db) {
|
|
7
|
+
const [rows] = await db.raw('SHOW TABLES');
|
|
8
|
+
const key = Object.keys(rows[0] || {})[0];
|
|
9
|
+
return rows.map((r) => r[key]).filter((n) => !EXCLUDED.has(n));
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
async foreignKeys(db, tableName) {
|
|
13
|
+
const [rows] = await db.raw(
|
|
14
|
+
`
|
|
15
|
+
SELECT
|
|
16
|
+
COLUMN_NAME AS \`from\`,
|
|
17
|
+
REFERENCED_TABLE_NAME AS \`table\`,
|
|
18
|
+
REFERENCED_COLUMN_NAME AS \`to\`
|
|
19
|
+
FROM information_schema.KEY_COLUMN_USAGE
|
|
20
|
+
WHERE TABLE_NAME = ?
|
|
21
|
+
AND TABLE_SCHEMA = DATABASE()
|
|
22
|
+
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
23
|
+
`,
|
|
24
|
+
[tableName],
|
|
25
|
+
);
|
|
26
|
+
return rows.map((r) => ({ from: r.from, table: r.table, column: r.to }));
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const EXCLUDED = new Set(['knex_migrations', 'knex_migrations_lock']);
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
clients: ['pg'],
|
|
5
|
+
|
|
6
|
+
async tables(db) {
|
|
7
|
+
const result = await db.raw(
|
|
8
|
+
"SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename",
|
|
9
|
+
);
|
|
10
|
+
return result.rows.map((r) => r.tablename).filter((n) => !EXCLUDED.has(n));
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
async foreignKeys(db, tableName) {
|
|
14
|
+
const result = await db.raw(
|
|
15
|
+
`
|
|
16
|
+
SELECT
|
|
17
|
+
kcu.column_name AS "from",
|
|
18
|
+
ccu.table_name AS "table",
|
|
19
|
+
ccu.column_name AS "to"
|
|
20
|
+
FROM information_schema.key_column_usage kcu
|
|
21
|
+
JOIN information_schema.referential_constraints rc
|
|
22
|
+
ON kcu.constraint_name = rc.constraint_name
|
|
23
|
+
AND kcu.constraint_schema = rc.constraint_schema
|
|
24
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
25
|
+
ON rc.unique_constraint_name = ccu.constraint_name
|
|
26
|
+
AND rc.unique_constraint_schema = ccu.constraint_schema
|
|
27
|
+
WHERE kcu.table_name = ?
|
|
28
|
+
AND kcu.table_schema = 'public'
|
|
29
|
+
`,
|
|
30
|
+
[tableName],
|
|
31
|
+
);
|
|
32
|
+
return result.rows.map((r) => ({ from: r.from, table: r.table, column: r.to }));
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const EXCLUDED = new Set(['knex_migrations', 'knex_migrations_lock', 'sqlite_sequence']);
|
|
2
|
+
|
|
3
|
+
function assertSafeIdentifier(name) {
|
|
4
|
+
if (!/^[a-zA-Z_]\w*$/.test(name)) {
|
|
5
|
+
throw new Error(`Unsafe table name: ${JSON.stringify(name)}`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
clients: ['better-sqlite3', 'sqlite3'],
|
|
11
|
+
|
|
12
|
+
async tables(db) {
|
|
13
|
+
const rows = await db.raw("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name");
|
|
14
|
+
return rows.map((r) => r.name).filter((n) => !EXCLUDED.has(n) && !n.startsWith('_'));
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
async foreignKeys(db, tableName) {
|
|
18
|
+
assertSafeIdentifier(tableName);
|
|
19
|
+
const fks = await db.raw(`PRAGMA foreign_key_list('${tableName}')`);
|
|
20
|
+
return fks.map((fk) => ({ from: fk.from, table: fk.table, column: fk.to }));
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import sqlite from './drivers/sqlite.js';
|
|
2
|
+
import pg from './drivers/pg.js';
|
|
3
|
+
import mysql from './drivers/mysql.js';
|
|
4
|
+
|
|
5
|
+
const drivers = [sqlite, pg, mysql];
|
|
6
|
+
|
|
7
|
+
function getDriver(client) {
|
|
8
|
+
for (const driver of drivers) {
|
|
9
|
+
if (driver.clients.includes(client)) return driver;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ── Introspection ──
|
|
15
|
+
|
|
16
|
+
function formatType(type, maxLength) {
|
|
17
|
+
if (maxLength != null && maxLength !== '' && Number(maxLength) > 0) {
|
|
18
|
+
return `${type}(${maxLength})`;
|
|
19
|
+
}
|
|
20
|
+
return type;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function introspectTable(db, tableName, driver) {
|
|
24
|
+
const columnInfo = await db(tableName).columnInfo();
|
|
25
|
+
const fks = driver ? await driver.foreignKeys(db, tableName) : [];
|
|
26
|
+
const fkMap = new Map(fks.map((fk) => [fk.from, { table: fk.table, column: fk.column }]));
|
|
27
|
+
|
|
28
|
+
return Object.entries(columnInfo).map(([name, info]) => {
|
|
29
|
+
const col = {
|
|
30
|
+
name,
|
|
31
|
+
type: formatType(info.type, info.maxLength),
|
|
32
|
+
nullable: info.nullable,
|
|
33
|
+
defaultValue: info.defaultValue != null ? String(info.defaultValue) : null,
|
|
34
|
+
};
|
|
35
|
+
const fk = fkMap.get(name);
|
|
36
|
+
if (fk) col.references = fk;
|
|
37
|
+
return col;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function introspectSchema(db) {
|
|
42
|
+
const client = db.client?.config?.client;
|
|
43
|
+
const driver = getDriver(client);
|
|
44
|
+
const tableNames = driver ? await driver.tables(db) : [];
|
|
45
|
+
const tables = [];
|
|
46
|
+
for (const tableName of tableNames) {
|
|
47
|
+
const columns = await introspectTable(db, tableName, driver);
|
|
48
|
+
tables.push({ name: tableName, columns });
|
|
49
|
+
}
|
|
50
|
+
tables.sort((a, b) => a.name.localeCompare(b.name));
|
|
51
|
+
return { tables };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Markdown generation ──
|
|
55
|
+
|
|
56
|
+
function generateSchemaMarkdown(schema) {
|
|
57
|
+
const lines = [];
|
|
58
|
+
lines.push('# Database Schema');
|
|
59
|
+
lines.push('');
|
|
60
|
+
lines.push('> Auto-generated by `arcway schema`. Do not edit manually.');
|
|
61
|
+
lines.push('');
|
|
62
|
+
for (const table of schema.tables) {
|
|
63
|
+
lines.push(`## ${table.name}`);
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push('| Column | Type | Nullable | Default | References |');
|
|
66
|
+
lines.push('|--------|------|----------|---------|------------|');
|
|
67
|
+
for (const col of table.columns) {
|
|
68
|
+
const nullable = col.nullable ? 'yes' : 'no';
|
|
69
|
+
const defaultVal = col.defaultValue ?? '';
|
|
70
|
+
const ref = col.references ? `${col.references.table}.${col.references.column}` : '';
|
|
71
|
+
lines.push(`| ${col.name} | ${col.type} | ${nullable} | ${defaultVal} | ${ref} |`);
|
|
72
|
+
}
|
|
73
|
+
lines.push('');
|
|
74
|
+
}
|
|
75
|
+
return lines.join('\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { generateSchemaMarkdown, introspectSchema };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { discoverModules } from '../discovery.js';
|
|
2
|
+
|
|
3
|
+
async function runSeeds(db, seedsDir) {
|
|
4
|
+
const entries = await discoverModules(seedsDir, { label: 'seed file' });
|
|
5
|
+
const results = [];
|
|
6
|
+
for (const { name, filePath, module } of entries) {
|
|
7
|
+
const seedFn = module.default;
|
|
8
|
+
if (typeof seedFn !== 'function') {
|
|
9
|
+
throw new Error(`Seed file "${name}" must export a default function`);
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
await seedFn(db);
|
|
13
|
+
results.push({ name, status: 'completed' });
|
|
14
|
+
} catch (err) {
|
|
15
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
16
|
+
throw new Error(`Seed "${name}" failed: ${error.message}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return results;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { runSeeds };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { glob } from './glob.js';
|
|
4
|
+
import { loadModule } from './module-loader.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Discover files in a directory, optionally recursing into subdirectories.
|
|
8
|
+
* Skips files starting with underscore by default.
|
|
9
|
+
* Returns sorted array of { name, filePath, relativePath }.
|
|
10
|
+
*/
|
|
11
|
+
async function discoverFiles(dir, { recursive = false, extensions = ['.js'], skipUnderscore = true, filter } = {}) {
|
|
12
|
+
if (!dir) return [];
|
|
13
|
+
|
|
14
|
+
let files;
|
|
15
|
+
if (recursive) {
|
|
16
|
+
const extGlob = extensions.length === 1 ? `*${extensions[0]}` : `*{${extensions.join(',')}}`;
|
|
17
|
+
files = await glob(path.join(dir, '**', extGlob));
|
|
18
|
+
} else {
|
|
19
|
+
let entries;
|
|
20
|
+
try {
|
|
21
|
+
entries = await fs.readdir(dir);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (err.code === 'ENOENT') return [];
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
const extSet = new Set(extensions);
|
|
27
|
+
files = entries
|
|
28
|
+
.filter((name) => extSet.has(path.extname(name)))
|
|
29
|
+
.map((name) => path.join(dir, name));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const results = [];
|
|
33
|
+
for (const filePath of files) {
|
|
34
|
+
const name = path.basename(filePath);
|
|
35
|
+
if (skipUnderscore && name.startsWith('_')) continue;
|
|
36
|
+
if (filter && !filter(name)) continue;
|
|
37
|
+
results.push({
|
|
38
|
+
name,
|
|
39
|
+
filePath,
|
|
40
|
+
relativePath: path.relative(dir, filePath),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
results.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
44
|
+
return results;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Discover and load modules from a directory.
|
|
49
|
+
* Returns sorted array of { name, filePath, relativePath, module }.
|
|
50
|
+
* Throws with a descriptive message if any file fails to import.
|
|
51
|
+
*/
|
|
52
|
+
async function discoverModules(dir, { recursive = false, extensions = ['.js'], skipUnderscore = true, filter, label = 'file' } = {}) {
|
|
53
|
+
const files = await discoverFiles(dir, { recursive, extensions, skipUnderscore, filter });
|
|
54
|
+
const results = [];
|
|
55
|
+
for (const entry of files) {
|
|
56
|
+
let module;
|
|
57
|
+
try {
|
|
58
|
+
module = await loadModule(entry.filePath, import.meta.url);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
throw new Error(`Failed to load ${label} at ${entry.filePath}: ${err}`);
|
|
61
|
+
}
|
|
62
|
+
results.push({ ...entry, module });
|
|
63
|
+
}
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { discoverFiles, discoverModules };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
function typeToOpenAPISchema(schema) {
|
|
2
|
+
const jsonSchema = schema.toJsonSchema();
|
|
3
|
+
const { $schema: _$schema, ...rest } = jsonSchema;
|
|
4
|
+
return rest;
|
|
5
|
+
}
|
|
6
|
+
function toOpenAPIPath(pattern) {
|
|
7
|
+
return pattern.replace(/:([^/]+)/g, '{$1}');
|
|
8
|
+
}
|
|
9
|
+
function buildPathParameters(route) {
|
|
10
|
+
const params = [];
|
|
11
|
+
for (const name of route.paramNames) {
|
|
12
|
+
const param = {
|
|
13
|
+
name,
|
|
14
|
+
in: 'path',
|
|
15
|
+
required: true,
|
|
16
|
+
schema: { type: 'string' },
|
|
17
|
+
};
|
|
18
|
+
if (route.config.schema?.query) {
|
|
19
|
+
try {
|
|
20
|
+
const fullSchema = typeToOpenAPISchema(route.config.schema.query);
|
|
21
|
+
if (fullSchema.properties && typeof fullSchema.properties === 'object') {
|
|
22
|
+
const fieldSchema = fullSchema.properties[name];
|
|
23
|
+
if (fieldSchema) {
|
|
24
|
+
param.schema = fieldSchema;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
params.push(param);
|
|
30
|
+
}
|
|
31
|
+
return params;
|
|
32
|
+
}
|
|
33
|
+
function buildQueryParameters(route) {
|
|
34
|
+
if (!route.config.schema?.query) return [];
|
|
35
|
+
try {
|
|
36
|
+
const schema = typeToOpenAPISchema(route.config.schema.query);
|
|
37
|
+
if (!schema.properties || typeof schema.properties !== 'object') return [];
|
|
38
|
+
const required = new Set(schema.required ?? []);
|
|
39
|
+
const params = [];
|
|
40
|
+
for (const [name, fieldSchema] of Object.entries(schema.properties)) {
|
|
41
|
+
params.push({
|
|
42
|
+
name,
|
|
43
|
+
in: 'query',
|
|
44
|
+
required: required.has(name),
|
|
45
|
+
schema: fieldSchema,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return params;
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function generateOpenAPISpec(routes, info) {
|
|
54
|
+
const spec = {
|
|
55
|
+
openapi: '3.0.3',
|
|
56
|
+
info: {
|
|
57
|
+
title: info?.title ?? 'Arcway API',
|
|
58
|
+
version: info?.version ?? '1.0.0',
|
|
59
|
+
...(info?.description ? { description: info.description } : {}),
|
|
60
|
+
},
|
|
61
|
+
paths: {},
|
|
62
|
+
};
|
|
63
|
+
for (const route of routes) {
|
|
64
|
+
const openApiPath = toOpenAPIPath(route.pattern);
|
|
65
|
+
const method = route.method.toLowerCase();
|
|
66
|
+
if (!spec.paths[openApiPath]) {
|
|
67
|
+
spec.paths[openApiPath] = {};
|
|
68
|
+
}
|
|
69
|
+
const operation = {
|
|
70
|
+
operationId: `${method}_${openApiPath
|
|
71
|
+
.replace(/[{}\/]/g, '_')
|
|
72
|
+
.replace(/_+/g, '_')
|
|
73
|
+
.replace(/^_|_$/g, '')}`,
|
|
74
|
+
};
|
|
75
|
+
const meta = route.config.meta;
|
|
76
|
+
if (meta?.summary) operation.summary = meta.summary;
|
|
77
|
+
if (meta?.description) operation.description = meta.description;
|
|
78
|
+
if (meta?.tags && meta.tags.length > 0) {
|
|
79
|
+
operation.tags = meta.tags;
|
|
80
|
+
} else {
|
|
81
|
+
const firstSegment = route.pattern.split('/')[1] || 'default';
|
|
82
|
+
operation.tags = [firstSegment];
|
|
83
|
+
}
|
|
84
|
+
const parameters = [...buildPathParameters(route), ...buildQueryParameters(route)];
|
|
85
|
+
if (parameters.length > 0) {
|
|
86
|
+
operation.parameters = parameters;
|
|
87
|
+
}
|
|
88
|
+
if (route.config.schema?.body) {
|
|
89
|
+
try {
|
|
90
|
+
const bodySchema = typeToOpenAPISchema(route.config.schema.body);
|
|
91
|
+
operation.requestBody = {
|
|
92
|
+
required: true,
|
|
93
|
+
content: {
|
|
94
|
+
'application/json': {
|
|
95
|
+
schema: bodySchema,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
const responseSchema = route.config.schema?.response
|
|
102
|
+
? (() => {
|
|
103
|
+
try {
|
|
104
|
+
return typeToOpenAPISchema(route.config.schema.response);
|
|
105
|
+
} catch {
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
108
|
+
})()
|
|
109
|
+
: void 0;
|
|
110
|
+
operation.responses = {
|
|
111
|
+
200: {
|
|
112
|
+
description: 'Successful response',
|
|
113
|
+
content: {
|
|
114
|
+
'application/json': {
|
|
115
|
+
schema: responseSchema ?? {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
data: {},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
...(route.config.schema?.body || route.config.schema?.query
|
|
125
|
+
? {
|
|
126
|
+
400: {
|
|
127
|
+
description: 'Validation error',
|
|
128
|
+
content: {
|
|
129
|
+
'application/json': {
|
|
130
|
+
schema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
error: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
code: { type: 'string' },
|
|
137
|
+
message: { type: 'string' },
|
|
138
|
+
details: {},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
: {}),
|
|
148
|
+
};
|
|
149
|
+
spec.paths[openApiPath][method] = operation;
|
|
150
|
+
}
|
|
151
|
+
return spec;
|
|
152
|
+
}
|
|
153
|
+
export { generateOpenAPISpec, toOpenAPIPath, typeToOpenAPISchema };
|