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
package/README.md
ADDED
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
# Arcway
|
|
2
|
+
|
|
3
|
+
A convention-based TypeScript framework for building modular monoliths with strict domain boundaries.
|
|
4
|
+
|
|
5
|
+
Arcway uses file-system conventions to discover routes, services, jobs, events, and middleware — no manual wiring required. Each domain is isolated with its own database scope, event emitter, queue, cache, and file storage.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install arcway
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Create a project:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
my-app/
|
|
17
|
+
├── arcway.config.ts
|
|
18
|
+
├── domains/
|
|
19
|
+
│ └── users/
|
|
20
|
+
│ ├── config.ts
|
|
21
|
+
│ └── api/
|
|
22
|
+
│ └── index.ts
|
|
23
|
+
└── db/
|
|
24
|
+
└── migrations/
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**arcway.config.ts**
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import type { FrameworkConfig } from 'arcway';
|
|
31
|
+
|
|
32
|
+
export default {
|
|
33
|
+
database: {
|
|
34
|
+
client: 'sqlite',
|
|
35
|
+
connection: './dev.db',
|
|
36
|
+
},
|
|
37
|
+
} satisfies FrameworkConfig;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**domains/users/config.ts**
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import type { DomainConfig } from 'arcway';
|
|
44
|
+
|
|
45
|
+
export default {
|
|
46
|
+
name: 'users',
|
|
47
|
+
tables: ['users'],
|
|
48
|
+
} satisfies DomainConfig;
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**domains/users/api/index.ts**
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import type { RouteMethodConfig } from 'arcway';
|
|
55
|
+
|
|
56
|
+
export const GET: RouteMethodConfig = {
|
|
57
|
+
handler: async (ctx) => {
|
|
58
|
+
const users = await ctx.db('users').select('*');
|
|
59
|
+
return { data: users };
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const POST: RouteMethodConfig = {
|
|
64
|
+
handler: async (ctx) => {
|
|
65
|
+
const [id] = await ctx.db('users').insert(ctx.body);
|
|
66
|
+
return { status: 201, data: { id } };
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Start the dev server:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx arcway dev
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This boots the full stack: database, migrations, route discovery, event bus, job runner, and HTTP server.
|
|
78
|
+
|
|
79
|
+
## CLI Commands
|
|
80
|
+
|
|
81
|
+
| Command | Description |
|
|
82
|
+
| ------------------------------- | ------------------------------------------------------------------ |
|
|
83
|
+
| `arcway dev` | Start development server (console logging, CORS enabled) |
|
|
84
|
+
| `arcway start` | Start production server (JSON logging, health check at `/health`) |
|
|
85
|
+
| `arcway build [outDir]` | Compile TypeScript to production bundle (default: `dist`) |
|
|
86
|
+
| `arcway seed` | Run database seed files from `db/seeds/` |
|
|
87
|
+
| `arcway docs [outFile]` | Generate OpenAPI spec from route schemas (default: `openapi.json`) |
|
|
88
|
+
| `arcway test [--watch] [pattern]` | Run domain tests (`domains/*/tests/**/*.test.ts`) |
|
|
89
|
+
| `arcway lint` | Check for domain boundary violations |
|
|
90
|
+
|
|
91
|
+
## Project Structure
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
project-root/
|
|
95
|
+
├── arcway.config.ts # Framework configuration
|
|
96
|
+
├── domains/
|
|
97
|
+
│ ├── users/
|
|
98
|
+
│ │ ├── config.ts # Domain name, owned tables, events, hooks
|
|
99
|
+
│ │ ├── services/index.ts # Exported service functions
|
|
100
|
+
│ │ ├── api/ # HTTP route handlers
|
|
101
|
+
│ │ │ ├── index.ts # GET /users, POST /users
|
|
102
|
+
│ │ │ ├── [id].ts # GET /users/:id, PUT /users/:id
|
|
103
|
+
│ │ │ ├── [id]/projects.ts # GET /users/:id/projects
|
|
104
|
+
│ │ │ ├── admin/settings.ts # GET /users/admin/settings
|
|
105
|
+
│ │ │ ├── _middleware.ts # Middleware for all /users/* routes
|
|
106
|
+
│ │ │ └── admin/
|
|
107
|
+
│ │ │ └── _middleware.ts # Middleware for /users/admin/* only
|
|
108
|
+
│ │ ├── jobs/ # Background job definitions
|
|
109
|
+
│ │ │ └── send-welcome.ts
|
|
110
|
+
│ │ ├── listeners/ # Event subscribers
|
|
111
|
+
│ │ │ └── on-signup.ts
|
|
112
|
+
│ │ └── tests/ # Domain unit tests
|
|
113
|
+
│ │ └── users.test.ts
|
|
114
|
+
│ └── billing/
|
|
115
|
+
│ ├── config.ts
|
|
116
|
+
│ ├── services/index.ts
|
|
117
|
+
│ └── listeners/
|
|
118
|
+
│ └── on-user-created.ts
|
|
119
|
+
└── db/
|
|
120
|
+
├── migrations/ # Knex migration files
|
|
121
|
+
│ └── 001_create_tables.js
|
|
122
|
+
└── seeds/ # Database seed files
|
|
123
|
+
└── 001_users.ts
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Files starting with `_` (except `_middleware.ts`) are excluded from route/job/listener discovery.
|
|
127
|
+
|
|
128
|
+
## Configuration
|
|
129
|
+
|
|
130
|
+
### Framework Config
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import type { FrameworkConfig } from 'arcway';
|
|
134
|
+
|
|
135
|
+
export default {
|
|
136
|
+
server: {
|
|
137
|
+
port: 3000,
|
|
138
|
+
shutdownTimeoutMs: 10_000,
|
|
139
|
+
maxBodySize: 1_048_576, // 1 MB
|
|
140
|
+
},
|
|
141
|
+
api: {
|
|
142
|
+
cors: {
|
|
143
|
+
// Or true/false
|
|
144
|
+
origin: ['https://app.com'],
|
|
145
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
146
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
147
|
+
exposedHeaders: ['X-Request-Id'],
|
|
148
|
+
credentials: true,
|
|
149
|
+
maxAge: 86400,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
database: {
|
|
153
|
+
client: 'postgres',
|
|
154
|
+
connection: 'postgres://user:pass@localhost/mydb',
|
|
155
|
+
},
|
|
156
|
+
queue: {
|
|
157
|
+
driver: 'redis', // 'knex' (default) or 'redis'
|
|
158
|
+
lockCooldownMs: 300_000,
|
|
159
|
+
redis: { url: 'redis://localhost:6379' },
|
|
160
|
+
},
|
|
161
|
+
cache: {
|
|
162
|
+
driver: 'redis',
|
|
163
|
+
defaultTtlMs: 60_000,
|
|
164
|
+
redis: { url: 'redis://localhost:6379' },
|
|
165
|
+
},
|
|
166
|
+
events: {
|
|
167
|
+
driver: 'redis', // 'memory' (default) or 'redis'
|
|
168
|
+
redis: { url: 'redis://localhost:6379' },
|
|
169
|
+
},
|
|
170
|
+
files: {
|
|
171
|
+
driver: 's3', // 'local' (default) or 's3'
|
|
172
|
+
s3: {
|
|
173
|
+
bucket: 'my-bucket',
|
|
174
|
+
region: 'us-east-1',
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
} satisfies FrameworkConfig;
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### CORS
|
|
181
|
+
|
|
182
|
+
CORS behavior by default:
|
|
183
|
+
|
|
184
|
+
- **Development** (`arcway dev`): Permissive — all origins allowed.
|
|
185
|
+
- **Production** (`arcway start`): Disabled — no CORS headers unless configured.
|
|
186
|
+
|
|
187
|
+
Configure with `api.cors`:
|
|
188
|
+
|
|
189
|
+
- `true` — enable permissive CORS in any mode.
|
|
190
|
+
- `false` — disable CORS entirely.
|
|
191
|
+
- `CorsConfig` object — use specific settings.
|
|
192
|
+
|
|
193
|
+
### Domain Config
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import type { DomainConfig } from 'arcway';
|
|
197
|
+
import { z } from 'zod';
|
|
198
|
+
|
|
199
|
+
export default {
|
|
200
|
+
name: 'users',
|
|
201
|
+
tables: ['users', 'sessions'],
|
|
202
|
+
events: [
|
|
203
|
+
{ name: 'users/created', schema: z.object({ userId: z.number() }) },
|
|
204
|
+
{ name: 'users/deleted' },
|
|
205
|
+
],
|
|
206
|
+
hooks: {
|
|
207
|
+
onInit: async (ctx) => {
|
|
208
|
+
/* Before server starts */
|
|
209
|
+
},
|
|
210
|
+
onReady: async (ctx) => {
|
|
211
|
+
/* After server is listening */
|
|
212
|
+
},
|
|
213
|
+
onShutdown: async (ctx) => {
|
|
214
|
+
/* Graceful shutdown */
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
} satisfies DomainConfig;
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Routes
|
|
221
|
+
|
|
222
|
+
Route files live in `domains/<name>/api/` and map to URL patterns by file path:
|
|
223
|
+
|
|
224
|
+
| File | URL Pattern |
|
|
225
|
+
| ----------------------- | -------------------------- |
|
|
226
|
+
| `api/index.ts` | `/<domain>` |
|
|
227
|
+
| `api/[id].ts` | `/<domain>/:id` |
|
|
228
|
+
| `api/[id]/projects.ts` | `/<domain>/:id/projects` |
|
|
229
|
+
| `api/admin/settings.ts` | `/<domain>/admin/settings` |
|
|
230
|
+
|
|
231
|
+
Export named constants for each HTTP method:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
import { z } from 'zod';
|
|
235
|
+
import type { RouteMethodConfig } from 'arcway';
|
|
236
|
+
|
|
237
|
+
export const GET: RouteMethodConfig = {
|
|
238
|
+
schema: {
|
|
239
|
+
query: z.object({ page: z.coerce.number().default(1) }),
|
|
240
|
+
},
|
|
241
|
+
meta: {
|
|
242
|
+
summary: 'List users',
|
|
243
|
+
tags: ['users'],
|
|
244
|
+
},
|
|
245
|
+
handler: async (ctx) => {
|
|
246
|
+
const users = await ctx
|
|
247
|
+
.db('users')
|
|
248
|
+
.select('*')
|
|
249
|
+
.limit(20)
|
|
250
|
+
.offset((ctx.query.page - 1) * 20);
|
|
251
|
+
return { data: users };
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
export const POST: RouteMethodConfig = {
|
|
256
|
+
schema: {
|
|
257
|
+
body: z.object({
|
|
258
|
+
name: z.string().min(1),
|
|
259
|
+
email: z.string().email(),
|
|
260
|
+
}),
|
|
261
|
+
},
|
|
262
|
+
handler: async (ctx) => {
|
|
263
|
+
const { name, email } = ctx.body as { name: string; email: string };
|
|
264
|
+
const [id] = await ctx.db('users').insert({ name, email });
|
|
265
|
+
await ctx.events.emit('users/created', { userId: id });
|
|
266
|
+
return { status: 201, data: { id } };
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Route Context
|
|
272
|
+
|
|
273
|
+
Every handler receives a `RouteContext`:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
interface RouteContext {
|
|
277
|
+
domain: string; // Domain name
|
|
278
|
+
db: Knex; // Scoped database connection
|
|
279
|
+
services: ServicesProxy; // Cross-domain service calls
|
|
280
|
+
events: DomainEvents; // Event emission
|
|
281
|
+
queue: DomainQueue; // Persistent queue
|
|
282
|
+
cache: DomainCache; // Key-value cache
|
|
283
|
+
files: DomainFiles; // File storage
|
|
284
|
+
params: Record<string, string>; // URL parameters (:id)
|
|
285
|
+
query: Record<string, string>; // Query string (?key=value)
|
|
286
|
+
body: unknown; // Parsed request body
|
|
287
|
+
headers: Record<string, string>; // Request headers
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Route Response
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
interface RouteResponse {
|
|
295
|
+
status?: number; // Default: 200 (or 400 if error)
|
|
296
|
+
data?: unknown; // Wrapped in { data: ... }
|
|
297
|
+
error?: {
|
|
298
|
+
code: string;
|
|
299
|
+
message: string;
|
|
300
|
+
details?: unknown;
|
|
301
|
+
};
|
|
302
|
+
headers?: Record<string, string>;
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Middleware
|
|
307
|
+
|
|
308
|
+
Place `_middleware.ts` files in `api/` directories. Middleware applies to all routes at that level and below.
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// domains/users/api/_middleware.ts — applies to all /users/* routes
|
|
312
|
+
import type { MiddlewareFn } from 'arcway';
|
|
313
|
+
|
|
314
|
+
const logger: MiddlewareFn = async (ctx, next) => {
|
|
315
|
+
const start = Date.now();
|
|
316
|
+
const response = await next();
|
|
317
|
+
console.log(`${ctx.headers['method']} took ${Date.now() - start}ms`);
|
|
318
|
+
return response;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
export default logger;
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// domains/users/api/admin/_middleware.ts — applies to /users/admin/* only
|
|
326
|
+
import type { MiddlewareFn } from 'arcway';
|
|
327
|
+
|
|
328
|
+
const auth: MiddlewareFn = async (ctx, next) => {
|
|
329
|
+
if (ctx.headers['authorization'] !== 'Bearer valid-token') {
|
|
330
|
+
return {
|
|
331
|
+
status: 401,
|
|
332
|
+
error: { code: 'UNAUTHORIZED', message: 'Invalid token' },
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
return next();
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
export default auth;
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Middleware can also export an array of functions:
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
export default [loggerMiddleware, authMiddleware];
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Middleware chains execute outermost-first. A request to `/users/admin/settings` runs:
|
|
348
|
+
|
|
349
|
+
1. `/users/api/_middleware.ts` (root)
|
|
350
|
+
2. `/users/api/admin/_middleware.ts` (admin)
|
|
351
|
+
3. Route handler
|
|
352
|
+
|
|
353
|
+
Return without calling `next()` to short-circuit (e.g., return 401).
|
|
354
|
+
|
|
355
|
+
## Services
|
|
356
|
+
|
|
357
|
+
Services enable cross-domain function calls with automatic context injection.
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
// domains/users/services/index.ts
|
|
361
|
+
import type { DomainContext } from 'arcway';
|
|
362
|
+
|
|
363
|
+
export default {
|
|
364
|
+
getById: async (ctx: DomainContext, id: string) => {
|
|
365
|
+
return ctx.db('users').where({ id }).first();
|
|
366
|
+
},
|
|
367
|
+
list: async (ctx: DomainContext) => {
|
|
368
|
+
return ctx.db('users').select('*');
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Call from another domain — `ctx` is injected automatically:
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
// In a billing route handler
|
|
377
|
+
const user = await ctx.services.users.getById(userId);
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Events
|
|
381
|
+
|
|
382
|
+
Domains declare events they can emit in `config.ts`:
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
events: [
|
|
386
|
+
{ name: 'users/created', schema: z.object({ userId: z.number() }) },
|
|
387
|
+
],
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Emit from any handler, service, or job:
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
const results = await ctx.events.emit('users/created', { userId: 42 });
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Always `await` the emit call — immediate handlers run in-process during emit and their results are returned as an array of `EventResult`.
|
|
397
|
+
|
|
398
|
+
### Listeners
|
|
399
|
+
|
|
400
|
+
Listeners define how they handle events via two handler functions:
|
|
401
|
+
|
|
402
|
+
- **`handler`** — dispatched via the event bus (eventually consistent, fire-and-forget)
|
|
403
|
+
- **`handlerImmediate`** — runs in-process during `emit()`, before bus dispatch
|
|
404
|
+
|
|
405
|
+
At least one must be defined. Both can be defined on the same listener.
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
// domains/billing/listeners/on-user-created.ts (async — via bus)
|
|
409
|
+
import type { ListenerDefinition } from 'arcway';
|
|
410
|
+
|
|
411
|
+
export default {
|
|
412
|
+
event: 'users/created',
|
|
413
|
+
handler: async (ctx, payload) => {
|
|
414
|
+
const { userId } = payload as { userId: number };
|
|
415
|
+
await ctx.db('billing_accounts').insert({ user_id: userId, balance: 0 });
|
|
416
|
+
},
|
|
417
|
+
} satisfies ListenerDefinition;
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
// domains/orgs/listeners/on-user-created.ts (immediate — in-process)
|
|
422
|
+
import type { ListenerDefinition } from 'arcway';
|
|
423
|
+
|
|
424
|
+
export default {
|
|
425
|
+
event: 'users/created',
|
|
426
|
+
handlerImmediate: async (ctx, payload) => {
|
|
427
|
+
const { userId } = payload as { userId: number };
|
|
428
|
+
const [org] = await ctx.db('orgs').insert({ owner_id: userId, name: 'My Org' });
|
|
429
|
+
if (!org) {
|
|
430
|
+
return { error: { code: 'ORG_CREATE_FAILED', message: 'Failed to create org' } };
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
} satisfies ListenerDefinition;
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Event patterns support wildcards: `'users/*'` matches all events from the users domain.
|
|
437
|
+
|
|
438
|
+
## Jobs
|
|
439
|
+
|
|
440
|
+
Background jobs support one-off queuing and cron scheduling.
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
// domains/billing/jobs/generate-invoice.ts
|
|
444
|
+
import { z } from 'zod';
|
|
445
|
+
import type { JobDefinition } from 'arcway';
|
|
446
|
+
|
|
447
|
+
export default {
|
|
448
|
+
name: 'generate-invoice',
|
|
449
|
+
schema: z.object({ userId: z.number(), month: z.string() }),
|
|
450
|
+
retries: 3,
|
|
451
|
+
schedule: '0 0 1 * *', // First of each month
|
|
452
|
+
handler: async (ctx, payload) => {
|
|
453
|
+
const { userId, month } = payload as { userId: number; month: string };
|
|
454
|
+
// Generate invoice...
|
|
455
|
+
},
|
|
456
|
+
} satisfies JobDefinition;
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Queue a job from any handler:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
await ctx.queue.push('generate-invoice', { userId: 42, month: '2025-01' });
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Failed jobs retry with exponential backoff (1s, 2s, 4s, 8s...).
|
|
466
|
+
|
|
467
|
+
## Queue
|
|
468
|
+
|
|
469
|
+
Domain-scoped persistent queue for background processing:
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
// Push work
|
|
473
|
+
await ctx.queue.push('email-send', { to: 'user@example.com', body: '...' });
|
|
474
|
+
|
|
475
|
+
// Pop and process (typically in a job handler)
|
|
476
|
+
const items = await ctx.queue.pop('email-send', 10);
|
|
477
|
+
for (const item of items) {
|
|
478
|
+
await sendEmail(item.data);
|
|
479
|
+
await ctx.queue.remove([item.id]);
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Drivers: `knex` (default, database-backed) or `redis`.
|
|
484
|
+
|
|
485
|
+
## Cache
|
|
486
|
+
|
|
487
|
+
Domain-scoped key-value cache with TTL support:
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
// Set with TTL
|
|
491
|
+
await ctx.cache.set('user:42', userData, 60_000);
|
|
492
|
+
|
|
493
|
+
// Get
|
|
494
|
+
const cached = await ctx.cache.get('user:42');
|
|
495
|
+
|
|
496
|
+
// Cache-aside pattern
|
|
497
|
+
const user = await ctx.cache.wrap(
|
|
498
|
+
'user:42',
|
|
499
|
+
async () => {
|
|
500
|
+
return ctx.db('users').where({ id: 42 }).first();
|
|
501
|
+
},
|
|
502
|
+
60_000,
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
// Delete
|
|
506
|
+
await ctx.cache.delete('user:42');
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Drivers: `knex` (default) or `redis`.
|
|
510
|
+
|
|
511
|
+
## File Storage
|
|
512
|
+
|
|
513
|
+
Domain-scoped file operations:
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
// Write
|
|
517
|
+
await ctx.files.write('avatars/user-42.png', imageBuffer);
|
|
518
|
+
|
|
519
|
+
// Read
|
|
520
|
+
const data = await ctx.files.read('avatars/user-42.png');
|
|
521
|
+
|
|
522
|
+
// List
|
|
523
|
+
const files = await ctx.files.list('avatars/');
|
|
524
|
+
|
|
525
|
+
// Check existence
|
|
526
|
+
const exists = await ctx.files.exists('avatars/user-42.png');
|
|
527
|
+
|
|
528
|
+
// Delete
|
|
529
|
+
await ctx.files.delete('avatars/user-42.png');
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Drivers: `local` (default, filesystem) or `s3`.
|
|
533
|
+
|
|
534
|
+
## Rate Limiting
|
|
535
|
+
|
|
536
|
+
Rate limiting is available as a middleware factory with pluggable backends:
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
// domains/api-gateway/api/_middleware.ts
|
|
540
|
+
import { createRateLimitMiddleware, MemoryRateLimitStore } from 'arcway';
|
|
541
|
+
|
|
542
|
+
const store = new MemoryRateLimitStore();
|
|
543
|
+
|
|
544
|
+
export default createRateLimitMiddleware(
|
|
545
|
+
{
|
|
546
|
+
max: 100, // 100 requests
|
|
547
|
+
windowMs: 60_000, // per minute
|
|
548
|
+
// keyFn: (ctx) => ctx.headers['x-api-key'] ?? 'anonymous',
|
|
549
|
+
// message: 'Rate limit exceeded',
|
|
550
|
+
},
|
|
551
|
+
store,
|
|
552
|
+
);
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
The middleware uses a sliding window algorithm. By default, it keys on the client IP from `X-Forwarded-For` or `X-Real-IP` headers.
|
|
556
|
+
|
|
557
|
+
Response headers are added automatically:
|
|
558
|
+
|
|
559
|
+
- `X-RateLimit-Limit` — configured maximum
|
|
560
|
+
- `X-RateLimit-Remaining` — remaining requests in window
|
|
561
|
+
- `X-RateLimit-Reset` — Unix timestamp when window resets
|
|
562
|
+
- `Retry-After` — seconds until retry (on 429 responses only)
|
|
563
|
+
|
|
564
|
+
Implement `RateLimitStore` for custom backends (Redis, etc.):
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
interface RateLimitStore {
|
|
568
|
+
check(key: string, max: number, windowMs: number): Promise<RateLimitResult>;
|
|
569
|
+
destroy(): void;
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
## Database Access Control
|
|
574
|
+
|
|
575
|
+
Domains can only write to tables listed in their `config.ts`. Attempting to write to another domain's table throws `DomainAccessViolation`:
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
// In users domain (tables: ['users', 'sessions'])
|
|
579
|
+
await ctx.db('users').insert({ name: 'Alice' }); // OK
|
|
580
|
+
await ctx.db('billing').insert({ amount: 100 }); // Throws DomainAccessViolation
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
Read access is unrestricted. Cross-domain writes go through services.
|
|
584
|
+
|
|
585
|
+
## Domain Boundary Linting
|
|
586
|
+
|
|
587
|
+
`arcway lint` statically checks for illegal imports between domains:
|
|
588
|
+
|
|
589
|
+
```bash
|
|
590
|
+
$ arcway lint
|
|
591
|
+
Checking domain boundaries...
|
|
592
|
+
Domains: users, billing, projects
|
|
593
|
+
Files scanned: 24
|
|
594
|
+
|
|
595
|
+
1 violation(s) found:
|
|
596
|
+
|
|
597
|
+
domains/billing/services/index.ts:3
|
|
598
|
+
Domain 'billing' imports directly from domain 'users'
|
|
599
|
+
Import: ../../users/services/index.ts
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Domains must communicate through `ctx.services`, `ctx.events`, or `ctx.queue` — never by importing each other's files directly.
|
|
603
|
+
|
|
604
|
+
## Testing
|
|
605
|
+
|
|
606
|
+
Arcway provides test utilities that create isolated domain contexts with in-memory SQLite:
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
610
|
+
import { createTestContext, type TestContext } from 'arcway';
|
|
611
|
+
|
|
612
|
+
describe('users service', () => {
|
|
613
|
+
let t: TestContext;
|
|
614
|
+
|
|
615
|
+
beforeAll(async () => {
|
|
616
|
+
t = await createTestContext({
|
|
617
|
+
domain: 'users',
|
|
618
|
+
tables: ['users'],
|
|
619
|
+
migrationsDir: 'db/migrations',
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
afterAll(() => t.cleanup());
|
|
624
|
+
|
|
625
|
+
it('creates a user', async () => {
|
|
626
|
+
const [id] = await t.ctx.db('users').insert({ name: 'Alice' });
|
|
627
|
+
const user = await t.ctx.db('users').where({ id }).first();
|
|
628
|
+
expect(user.name).toBe('Alice');
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
The test context stubs all infrastructure (events, queue, cache, files) so tests run fast and in isolation.
|
|
634
|
+
|
|
635
|
+
## Seeds
|
|
636
|
+
|
|
637
|
+
Seed files live in `db/seeds/` and run in alphabetical order:
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
// db/seeds/001_users.ts
|
|
641
|
+
import type { Knex } from 'knex';
|
|
642
|
+
|
|
643
|
+
export default async function seed(db: Knex): Promise<void> {
|
|
644
|
+
await db('users')
|
|
645
|
+
.insert([
|
|
646
|
+
{ id: 1, name: 'Alice', email: 'alice@test.com' },
|
|
647
|
+
{ id: 2, name: 'Bob', email: 'bob@test.com' },
|
|
648
|
+
])
|
|
649
|
+
.onConflict('id')
|
|
650
|
+
.merge();
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
Run with `arcway seed`. Migrations execute first to ensure the schema is current.
|
|
655
|
+
|
|
656
|
+
## OpenAPI Generation
|
|
657
|
+
|
|
658
|
+
`arcway docs` generates an OpenAPI 3.0 spec from route schemas:
|
|
659
|
+
|
|
660
|
+
```bash
|
|
661
|
+
arcway docs openapi.json
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
Routes with `meta` and `schema` fields produce documented endpoints:
|
|
665
|
+
|
|
666
|
+
```typescript
|
|
667
|
+
export const GET: RouteMethodConfig = {
|
|
668
|
+
schema: {
|
|
669
|
+
params: z.object({ id: z.string() }),
|
|
670
|
+
query: z.object({ fields: z.string().optional() }),
|
|
671
|
+
},
|
|
672
|
+
meta: {
|
|
673
|
+
summary: 'Get user by ID',
|
|
674
|
+
description: 'Returns a single user record.',
|
|
675
|
+
tags: ['users'],
|
|
676
|
+
},
|
|
677
|
+
handler: async (ctx) => {
|
|
678
|
+
/* ... */
|
|
679
|
+
},
|
|
680
|
+
};
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
## Production Build
|
|
684
|
+
|
|
685
|
+
```bash
|
|
686
|
+
arcway build # Compile to dist/
|
|
687
|
+
cd dist && arcway start
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
The build step compiles TypeScript with esbuild, copies non-TS files (migrations, JSON), and preserves the directory structure. The production server uses native `import()` — no tsx required at runtime.
|
|
691
|
+
|
|
692
|
+
## Boot Sequence
|
|
693
|
+
|
|
694
|
+
When `arcway dev` or `arcway start` runs, the framework:
|
|
695
|
+
|
|
696
|
+
1. Loads `arcway.config.ts`
|
|
697
|
+
2. Discovers all domains in `domains/`
|
|
698
|
+
3. Connects to the database and runs migrations
|
|
699
|
+
4. Initializes queue, cache, and file drivers
|
|
700
|
+
5. Creates scoped domain contexts
|
|
701
|
+
6. Discovers services and wires cross-domain proxies
|
|
702
|
+
7. Creates event bus and registers listeners
|
|
703
|
+
8. Discovers and registers jobs
|
|
704
|
+
9. Runs `onInit` hooks
|
|
705
|
+
10. Discovers routes and middleware
|
|
706
|
+
11. Resolves CORS configuration
|
|
707
|
+
12. Creates and starts HTTP server
|
|
708
|
+
13. Runs `onReady` hooks
|
|
709
|
+
14. Starts job runner (cron scheduler)
|
|
710
|
+
|
|
711
|
+
Graceful shutdown reverses the process: stops the job runner, runs `onShutdown` hooks, drains connections, and closes the server.
|