@archlast/server 0.1.0 → 0.1.2
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/README.md +55 -756
- package/package.json +2 -2
- package/templates/archlast.config.js +1 -1
- package/templates/docker-compose.yml +1 -1
package/README.md
CHANGED
|
@@ -1,801 +1,100 @@
|
|
|
1
1
|
# @archlast/server
|
|
2
2
|
|
|
3
|
-
Type-safe server definitions and runtime helpers for Archlast. This package
|
|
3
|
+
Type-safe server definitions and runtime helpers for Archlast. This package is
|
|
4
|
+
used to define schema, functions, and shared types, and it ships Docker
|
|
5
|
+
templates for runtime deployment.
|
|
4
6
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
- [Installation](#installation)
|
|
8
|
-
- [Library vs Runtime](#library-vs-runtime)
|
|
9
|
-
- [Schema Definition](#schema-definition)
|
|
10
|
-
- [Field Types (Validators)](#field-types-validators)
|
|
11
|
-
- [Field Modifiers](#field-modifiers)
|
|
12
|
-
- [Relationships](#relationships)
|
|
13
|
-
- [Indexes](#indexes)
|
|
14
|
-
- [Function Definitions](#function-definitions)
|
|
15
|
-
- [Query](#query)
|
|
16
|
-
- [Mutation](#mutation)
|
|
17
|
-
- [Action](#action)
|
|
18
|
-
- [HTTP Routes](#http-routes)
|
|
19
|
-
- [Webhooks](#webhooks)
|
|
20
|
-
- [RPC (tRPC-style)](#rpc-trpc-style)
|
|
21
|
-
- [Context Objects](#context-objects)
|
|
22
|
-
- [Authentication & Permissions](#authentication--permissions)
|
|
23
|
-
- [Background Jobs](#background-jobs)
|
|
24
|
-
- [Storage Types](#storage-types)
|
|
25
|
-
- [Subpath Exports](#subpath-exports)
|
|
26
|
-
- [Environment Variables](#environment-variables)
|
|
27
|
-
- [Docker Templates](#docker-templates)
|
|
28
|
-
|
|
29
|
-
## Installation
|
|
7
|
+
## Install
|
|
30
8
|
|
|
31
9
|
```bash
|
|
32
10
|
npm install -D @archlast/server
|
|
33
|
-
|
|
34
|
-
# Or with other package managers
|
|
35
|
-
pnpm add -D @archlast/server
|
|
36
|
-
yarn add -D @archlast/server
|
|
37
|
-
bun add -D @archlast/server
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Library vs Runtime
|
|
41
|
-
|
|
42
|
-
This package is a **dev-time library** for schema and function definitions. The actual server runtime is delivered via Docker and managed by the CLI:
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
# Use the CLI to run the server
|
|
46
|
-
archlast start
|
|
47
|
-
|
|
48
|
-
# The Docker image contains the full runtime
|
|
49
|
-
docker pull archlast/server:latest
|
|
50
11
|
```
|
|
51
12
|
|
|
52
|
-
|
|
13
|
+
## Library vs runtime
|
|
53
14
|
|
|
54
|
-
|
|
15
|
+
This package is a dev-time library for schema and function definitions. The
|
|
16
|
+
runtime server is delivered via the Docker image (`algochad/archlast-server`) and managed
|
|
17
|
+
by the CLI (`archlast start`).
|
|
55
18
|
|
|
56
|
-
|
|
19
|
+
## Schema
|
|
57
20
|
|
|
58
21
|
```ts
|
|
59
22
|
import { defineSchema, defineTable, v } from "@archlast/server/schema/definition";
|
|
60
23
|
|
|
61
24
|
export default defineSchema({
|
|
62
25
|
tasks: defineTable({
|
|
63
|
-
|
|
26
|
+
id: v.id(),
|
|
64
27
|
text: v.string(),
|
|
65
|
-
completed: v.boolean().default(false),
|
|
66
|
-
priority: v.string().default("medium"),
|
|
67
|
-
createdAt: v.number().createdNow(),
|
|
68
|
-
updatedAt: v.number().updateNow().optional(),
|
|
69
|
-
}),
|
|
70
|
-
|
|
71
|
-
users: defineTable({
|
|
72
|
-
_id: v.id().primaryKey(),
|
|
73
|
-
email: v.string().unique(),
|
|
74
|
-
name: v.string(),
|
|
75
|
-
role: v.string().default("user"),
|
|
76
28
|
}),
|
|
77
29
|
});
|
|
78
30
|
```
|
|
79
31
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
The `v` object provides all available field types:
|
|
83
|
-
|
|
84
|
-
| Validator | TypeScript Type | Description |
|
|
85
|
-
|-----------|-----------------|-------------|
|
|
86
|
-
| `v.id()` | `string` | CUID2 identifier (auto-generated) |
|
|
87
|
-
| `v.uuid()` | `string` | UUID v4 identifier |
|
|
88
|
-
| `v.objectId()` | `string` | MongoDB-style ObjectId |
|
|
89
|
-
| `v.string()` | `string` | Text field |
|
|
90
|
-
| `v.number()` | `number` | Numeric field (integer or float) |
|
|
91
|
-
| `v.boolean()` | `boolean` | True/false field |
|
|
92
|
-
| `v.date()` | `Date` | Date object |
|
|
93
|
-
| `v.datetime()` | `Date` | Date with time |
|
|
94
|
-
| `v.bytes()` | `Uint8Array` | Binary data |
|
|
95
|
-
| `v.json()` | `unknown` | Arbitrary JSON |
|
|
96
|
-
| `v.any()` | `any` | Any type (use sparingly) |
|
|
97
|
-
| `v.object(shape)` | `object` | Nested object with schema |
|
|
98
|
-
| `v.array(schema)` | `T[]` | Array of items |
|
|
99
|
-
| `v.optional(type)` | `T \| undefined` | Optional wrapper |
|
|
100
|
-
|
|
101
|
-
### Examples
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
import { v } from "@archlast/server/schema/validators";
|
|
105
|
-
|
|
106
|
-
// Basic types
|
|
107
|
-
v.string() // string
|
|
108
|
-
v.number() // number
|
|
109
|
-
v.boolean() // boolean
|
|
110
|
-
|
|
111
|
-
// IDs
|
|
112
|
-
v.id() // CUID2: "clxxxx..."
|
|
113
|
-
v.uuid() // UUID: "550e8400-e29b-41d4-a716-446655440000"
|
|
114
|
-
v.objectId() // ObjectId: "507f1f77bcf86cd799439011"
|
|
115
|
-
|
|
116
|
-
// Dates
|
|
117
|
-
v.date() // Date object (date only)
|
|
118
|
-
v.datetime() // Date object (with time)
|
|
119
|
-
|
|
120
|
-
// Complex types
|
|
121
|
-
v.json() // Any JSON-serializable value
|
|
122
|
-
v.bytes() // Binary data as Uint8Array
|
|
123
|
-
|
|
124
|
-
// Nested object
|
|
125
|
-
v.object({
|
|
126
|
-
street: v.string(),
|
|
127
|
-
city: v.string(),
|
|
128
|
-
zip: v.string(),
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
// Array
|
|
132
|
-
v.array(v.string()) // string[]
|
|
133
|
-
v.array(v.object({ // Array of objects
|
|
134
|
-
name: v.string(),
|
|
135
|
-
score: v.number(),
|
|
136
|
-
}))
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### Field Modifiers
|
|
140
|
-
|
|
141
|
-
Chain modifiers to add constraints and behaviors:
|
|
142
|
-
|
|
143
|
-
| Modifier | Description |
|
|
144
|
-
|----------|-------------|
|
|
145
|
-
| `.primaryKey()` | Mark as primary key (auto-generates if not provided) |
|
|
146
|
-
| `.unique()` | Unique constraint |
|
|
147
|
-
| `.index()` | Create index for faster queries |
|
|
148
|
-
| `.searchable()` | Enable full-text search |
|
|
149
|
-
| `.optional()` | Make field nullable (TypeScript: `T \| undefined`) |
|
|
150
|
-
| `.default(value)` | Default value on insert |
|
|
151
|
-
| `.createdNow()` | Auto-set to `Date.now()` on insert |
|
|
152
|
-
| `.updateNow()` | Auto-set to `Date.now()` on every update |
|
|
153
|
-
| `.foreignKey(table, field, options)` | Foreign key reference |
|
|
154
|
-
|
|
155
|
-
### Examples
|
|
156
|
-
|
|
157
|
-
```ts
|
|
158
|
-
defineTable({
|
|
159
|
-
// Primary key with auto-generation
|
|
160
|
-
_id: v.id().primaryKey(),
|
|
161
|
-
|
|
162
|
-
// Unique constraint
|
|
163
|
-
email: v.string().unique(),
|
|
164
|
-
|
|
165
|
-
// Indexed for fast lookups
|
|
166
|
-
slug: v.string().index(),
|
|
167
|
-
|
|
168
|
-
// Full-text searchable
|
|
169
|
-
content: v.string().searchable(),
|
|
170
|
-
|
|
171
|
-
// Optional field
|
|
172
|
-
bio: v.string().optional(),
|
|
173
|
-
|
|
174
|
-
// Default values
|
|
175
|
-
role: v.string().default("user"),
|
|
176
|
-
count: v.number().default(0),
|
|
177
|
-
active: v.boolean().default(true),
|
|
178
|
-
|
|
179
|
-
// Automatic timestamps
|
|
180
|
-
createdAt: v.number().createdNow(),
|
|
181
|
-
updatedAt: v.number().updateNow().optional(),
|
|
182
|
-
|
|
183
|
-
// Foreign key
|
|
184
|
-
authorId: v.id().foreignKey("users", "_id", {
|
|
185
|
-
onDelete: "cascade",
|
|
186
|
-
onUpdate: "cascade",
|
|
187
|
-
}),
|
|
188
|
-
})
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### Relationships
|
|
192
|
-
|
|
193
|
-
Define relationships in the second argument of `defineTable`:
|
|
32
|
+
## Functions
|
|
194
33
|
|
|
195
34
|
```ts
|
|
196
|
-
import {
|
|
197
|
-
|
|
198
|
-
hasOne,
|
|
199
|
-
hasMany,
|
|
200
|
-
belongsTo,
|
|
201
|
-
manyToMany
|
|
202
|
-
} from "@archlast/server/schema/definition";
|
|
203
|
-
|
|
204
|
-
export default defineSchema({
|
|
205
|
-
users: defineTable(
|
|
206
|
-
{
|
|
207
|
-
_id: v.id().primaryKey(),
|
|
208
|
-
name: v.string(),
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
relationships: {
|
|
212
|
-
// User has many tasks
|
|
213
|
-
tasks: hasMany("tasks", "authorId"),
|
|
35
|
+
import { query, mutation } from "@archlast/server/functions/definition";
|
|
36
|
+
import { z } from "zod";
|
|
214
37
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
},
|
|
218
|
-
}
|
|
219
|
-
),
|
|
220
|
-
|
|
221
|
-
tasks: defineTable(
|
|
222
|
-
{
|
|
223
|
-
_id: v.id().primaryKey(),
|
|
224
|
-
text: v.string(),
|
|
225
|
-
authorId: v.id(),
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
relationships: {
|
|
229
|
-
// Task belongs to user
|
|
230
|
-
author: belongsTo("users", "authorId"),
|
|
231
|
-
|
|
232
|
-
// Task has many subtasks
|
|
233
|
-
subtasks: hasMany("subtasks", "taskId"),
|
|
234
|
-
},
|
|
235
|
-
}
|
|
236
|
-
),
|
|
237
|
-
|
|
238
|
-
tags: defineTable(
|
|
239
|
-
{
|
|
240
|
-
_id: v.id().primaryKey(),
|
|
241
|
-
name: v.string(),
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
relationships: {
|
|
245
|
-
// Many-to-many through junction table
|
|
246
|
-
tasks: manyToMany("tasks", "task_tags", "tagId", "taskId"),
|
|
247
|
-
},
|
|
248
|
-
}
|
|
249
|
-
),
|
|
38
|
+
export const list = query({
|
|
39
|
+
handler: async (ctx) => ctx.db.table("tasks").findMany(),
|
|
250
40
|
});
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
### Indexes
|
|
254
|
-
|
|
255
|
-
Create composite indexes for complex queries:
|
|
256
|
-
|
|
257
|
-
```ts
|
|
258
|
-
defineTable(
|
|
259
|
-
{
|
|
260
|
-
_id: v.id().primaryKey(),
|
|
261
|
-
completed: v.boolean(),
|
|
262
|
-
priority: v.string(),
|
|
263
|
-
createdAt: v.number(),
|
|
264
|
-
},
|
|
265
|
-
{
|
|
266
|
-
indexes: [
|
|
267
|
-
// Composite index
|
|
268
|
-
{ fields: ["completed", "createdAt"], name: "idx_tasks_status" },
|
|
269
|
-
|
|
270
|
-
// Single field index
|
|
271
|
-
{ fields: ["priority"], name: "idx_tasks_priority" },
|
|
272
|
-
],
|
|
273
|
-
}
|
|
274
|
-
)
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
---
|
|
278
|
-
|
|
279
|
-
## Function Definitions
|
|
280
|
-
|
|
281
|
-
Functions are the API of your Archlast application. Import from `@archlast/server/functions/definition`.
|
|
282
|
-
|
|
283
|
-
### Query
|
|
284
|
-
|
|
285
|
-
Read-only operations. Results are cached and reactive.
|
|
286
|
-
|
|
287
|
-
```ts
|
|
288
|
-
import { query } from "@archlast/server/functions/definition";
|
|
289
|
-
|
|
290
|
-
// Simple query
|
|
291
|
-
export const list = query(async (ctx) => {
|
|
292
|
-
return ctx.db.query("tasks").findMany();
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
// Query with arguments
|
|
296
|
-
export const get = query({
|
|
297
|
-
args: { id: v.string().zodSchema },
|
|
298
|
-
handler: async (ctx, args) => {
|
|
299
|
-
return ctx.db.get("tasks", args.id);
|
|
300
|
-
},
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// Query with filtering
|
|
304
|
-
export const listByStatus = query({
|
|
305
|
-
args: {
|
|
306
|
-
completed: v.boolean().zodSchema,
|
|
307
|
-
limit: v.number().optional().zodSchema,
|
|
308
|
-
},
|
|
309
|
-
handler: async (ctx, args) => {
|
|
310
|
-
return ctx.db
|
|
311
|
-
.query("tasks")
|
|
312
|
-
.where({ completed: args.completed })
|
|
313
|
-
.take(args.limit ?? 10)
|
|
314
|
-
.findMany();
|
|
315
|
-
},
|
|
316
|
-
});
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
### Mutation
|
|
320
|
-
|
|
321
|
-
Write operations. Automatically invalidate related caches.
|
|
322
|
-
|
|
323
|
-
```ts
|
|
324
|
-
import { mutation } from "@archlast/server/functions/definition";
|
|
325
41
|
|
|
326
42
|
export const create = mutation({
|
|
327
|
-
args: {
|
|
328
|
-
|
|
329
|
-
priority: v.string().optional().zodSchema,
|
|
330
|
-
},
|
|
331
|
-
handler: async (ctx, args) => {
|
|
332
|
-
return ctx.db.insert("tasks", {
|
|
333
|
-
text: args.text,
|
|
334
|
-
priority: args.priority ?? "medium",
|
|
335
|
-
completed: false,
|
|
336
|
-
});
|
|
337
|
-
},
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
export const update = mutation({
|
|
341
|
-
args: {
|
|
342
|
-
id: v.string().zodSchema,
|
|
343
|
-
text: v.string().optional().zodSchema,
|
|
344
|
-
completed: v.boolean().optional().zodSchema,
|
|
345
|
-
},
|
|
346
|
-
handler: async (ctx, args) => {
|
|
347
|
-
const { id, ...data } = args;
|
|
348
|
-
return ctx.db.update("tasks", id, data);
|
|
349
|
-
},
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
export const remove = mutation({
|
|
353
|
-
args: { id: v.string().zodSchema },
|
|
354
|
-
handler: async (ctx, args) => {
|
|
355
|
-
await ctx.db.delete("tasks", args.id);
|
|
356
|
-
return { success: true };
|
|
357
|
-
},
|
|
358
|
-
});
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
### Action
|
|
362
|
-
|
|
363
|
-
Long-running operations, side effects, external API calls.
|
|
364
|
-
|
|
365
|
-
```ts
|
|
366
|
-
import { action } from "@archlast/server/functions/definition";
|
|
367
|
-
|
|
368
|
-
export const sendEmail = action({
|
|
369
|
-
args: {
|
|
370
|
-
to: v.string().zodSchema,
|
|
371
|
-
subject: v.string().zodSchema,
|
|
372
|
-
body: v.string().zodSchema,
|
|
373
|
-
},
|
|
374
|
-
handler: async (ctx, args) => {
|
|
375
|
-
// Call external email service
|
|
376
|
-
const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
|
|
377
|
-
method: "POST",
|
|
378
|
-
headers: {
|
|
379
|
-
"Authorization": `Bearer ${process.env.SENDGRID_API_KEY}`,
|
|
380
|
-
"Content-Type": "application/json",
|
|
381
|
-
},
|
|
382
|
-
body: JSON.stringify({
|
|
383
|
-
personalizations: [{ to: [{ email: args.to }] }],
|
|
384
|
-
from: { email: "noreply@myapp.com" },
|
|
385
|
-
subject: args.subject,
|
|
386
|
-
content: [{ type: "text/plain", value: args.body }],
|
|
387
|
-
}),
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
return { sent: response.ok };
|
|
391
|
-
},
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
export const processImage = action({
|
|
395
|
-
args: { storageId: v.string().zodSchema },
|
|
396
|
-
handler: async (ctx, args) => {
|
|
397
|
-
const file = await ctx.storage.get(args.storageId);
|
|
398
|
-
// Process image...
|
|
399
|
-
const processedId = await ctx.storage.store("processed", processedBuffer);
|
|
400
|
-
return { processedId };
|
|
401
|
-
},
|
|
402
|
-
});
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
### HTTP Routes
|
|
406
|
-
|
|
407
|
-
Explicit HTTP endpoints with full control over request/response.
|
|
408
|
-
|
|
409
|
-
```ts
|
|
410
|
-
import { http } from "@archlast/server/functions/definition";
|
|
411
|
-
|
|
412
|
-
// GET route
|
|
413
|
-
export const health = http.get({
|
|
414
|
-
path: "/health",
|
|
415
|
-
handler: async () => {
|
|
416
|
-
return new Response("ok", { status: 200 });
|
|
417
|
-
},
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
// POST route with body parsing
|
|
421
|
-
export const createWebhook = http.post({
|
|
422
|
-
path: "/api/webhooks",
|
|
423
|
-
handler: async (ctx, request) => {
|
|
424
|
-
const body = await request.json();
|
|
425
|
-
// Process webhook...
|
|
426
|
-
return Response.json({ received: true });
|
|
427
|
-
},
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
// Route with path parameters
|
|
431
|
-
export const getUser = http.get({
|
|
432
|
-
path: "/api/users/:id",
|
|
433
|
-
handler: async (ctx, request, params) => {
|
|
434
|
-
const user = await ctx.db.get("users", params.id);
|
|
435
|
-
if (!user) {
|
|
436
|
-
return new Response("Not found", { status: 404 });
|
|
437
|
-
}
|
|
438
|
-
return Response.json(user);
|
|
439
|
-
},
|
|
43
|
+
args: { text: z.string() },
|
|
44
|
+
handler: async (ctx, args) => ctx.db.table("tasks").insert({ text: args.text }),
|
|
440
45
|
});
|
|
441
|
-
|
|
442
|
-
// Other methods
|
|
443
|
-
http.put({ path: "/api/resource/:id", handler: async (ctx, req) => { /* ... */ } });
|
|
444
|
-
http.patch({ path: "/api/resource/:id", handler: async (ctx, req) => { /* ... */ } });
|
|
445
|
-
http.delete({ path: "/api/resource/:id", handler: async (ctx, req) => { /* ... */ } });
|
|
446
|
-
http.options({ path: "/api/resource", handler: async () => { /* ... */ } });
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
### Webhooks
|
|
450
|
-
|
|
451
|
-
Signed webhook handlers with automatic verification.
|
|
452
|
-
|
|
453
|
-
```ts
|
|
454
|
-
import { webhook } from "@archlast/server/functions/definition";
|
|
455
|
-
|
|
456
|
-
export const stripeWebhook = webhook({
|
|
457
|
-
path: "/webhooks/stripe",
|
|
458
|
-
secret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
459
|
-
handler: async (ctx, event) => {
|
|
460
|
-
switch (event.type) {
|
|
461
|
-
case "checkout.session.completed":
|
|
462
|
-
await handleCheckoutComplete(ctx, event.data);
|
|
463
|
-
break;
|
|
464
|
-
case "customer.subscription.deleted":
|
|
465
|
-
await handleSubscriptionCanceled(ctx, event.data);
|
|
466
|
-
break;
|
|
467
|
-
}
|
|
468
|
-
return { received: true };
|
|
469
|
-
},
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
export const githubWebhook = webhook({
|
|
473
|
-
path: "/webhooks/github",
|
|
474
|
-
secret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
475
|
-
signatureHeader: "X-Hub-Signature-256",
|
|
476
|
-
handler: async (ctx, payload) => {
|
|
477
|
-
console.log("GitHub event:", payload.action);
|
|
478
|
-
return { ok: true };
|
|
479
|
-
},
|
|
480
|
-
});
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
### RPC (tRPC-style)
|
|
484
|
-
|
|
485
|
-
Type-safe RPC procedures with automatic router generation.
|
|
486
|
-
|
|
487
|
-
```ts
|
|
488
|
-
import { rpc } from "@archlast/server/functions/definition";
|
|
489
|
-
|
|
490
|
-
// RPC query
|
|
491
|
-
export const getTasks = rpc.query({
|
|
492
|
-
args: { completed: v.boolean().optional().zodSchema },
|
|
493
|
-
handler: async (ctx, args) => {
|
|
494
|
-
return ctx.db.query("tasks")
|
|
495
|
-
.where(args.completed !== undefined ? { completed: args.completed } : {})
|
|
496
|
-
.findMany();
|
|
497
|
-
},
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
// RPC mutation
|
|
501
|
-
export const createTask = rpc.mutation({
|
|
502
|
-
args: { text: v.string().zodSchema },
|
|
503
|
-
handler: async (ctx, args) => {
|
|
504
|
-
return ctx.db.insert("tasks", { text: args.text });
|
|
505
|
-
},
|
|
506
|
-
});
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
---
|
|
510
|
-
|
|
511
|
-
## Context Objects
|
|
512
|
-
|
|
513
|
-
Each function receives a context object with available services.
|
|
514
|
-
|
|
515
|
-
### QueryCtx (for queries)
|
|
516
|
-
|
|
517
|
-
```ts
|
|
518
|
-
interface QueryCtx {
|
|
519
|
-
db: GenericDatabaseReader<DataModel>;
|
|
520
|
-
auth: AuthContext;
|
|
521
|
-
repository: RepositoryMap<DataModel>;
|
|
522
|
-
logger: LogService;
|
|
523
|
-
}
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
### MutationCtx (for mutations)
|
|
527
|
-
|
|
528
|
-
```ts
|
|
529
|
-
interface MutationCtx extends QueryCtx {
|
|
530
|
-
db: GenericDatabaseWriter<DataModel>;
|
|
531
|
-
scheduler: JobScheduler;
|
|
532
|
-
}
|
|
533
46
|
```
|
|
534
47
|
|
|
535
|
-
|
|
48
|
+
Other function types:
|
|
49
|
+
- `action` for long running tasks
|
|
50
|
+
- `http` for explicit HTTP routes
|
|
51
|
+
- `webhook` for signed incoming events
|
|
52
|
+
- `rpc` for tRPC-style public procedures
|
|
536
53
|
|
|
537
|
-
|
|
538
|
-
interface ActionCtx extends MutationCtx {
|
|
539
|
-
storage: StorageClient;
|
|
540
|
-
}
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
### AuthContext
|
|
544
|
-
|
|
545
|
-
```ts
|
|
546
|
-
interface AuthContext {
|
|
547
|
-
userId: string | null;
|
|
548
|
-
sessionId: string | null;
|
|
549
|
-
isAuthenticated: boolean;
|
|
550
|
-
user: User | null;
|
|
551
|
-
}
|
|
552
|
-
```
|
|
54
|
+
## Auth and permissions
|
|
553
55
|
|
|
554
|
-
|
|
56
|
+
All functions default to `auth: "required"`. You can mark functions public or
|
|
57
|
+
optional, and attach permissions.
|
|
555
58
|
|
|
556
59
|
```ts
|
|
557
|
-
export const
|
|
558
|
-
// Database access
|
|
559
|
-
const tasks = await ctx.db.query("tasks").findMany();
|
|
560
|
-
|
|
561
|
-
// LINQ-style repository
|
|
562
|
-
const data = await ctx.repository.tasks
|
|
563
|
-
.Where({ completed: false })
|
|
564
|
-
.OrderBy({ createdAt: "desc" })
|
|
565
|
-
.Take(10)
|
|
566
|
-
.ToArrayAsync();
|
|
567
|
-
|
|
568
|
-
// Auth info
|
|
569
|
-
if (ctx.auth.isAuthenticated) {
|
|
570
|
-
console.log("User:", ctx.auth.userId);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Logging
|
|
574
|
-
ctx.logger.info("Query executed", { count: tasks.length });
|
|
575
|
-
|
|
576
|
-
return tasks;
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
export const myMutation = mutation(async (ctx) => {
|
|
580
|
-
// Schedule background job
|
|
581
|
-
await ctx.scheduler.schedule({
|
|
582
|
-
name: "sendNotification",
|
|
583
|
-
preset: SchedulePreset.RunNow,
|
|
584
|
-
payload: { userId: ctx.auth.userId },
|
|
585
|
-
});
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
export const myAction = action(async (ctx) => {
|
|
589
|
-
// File storage
|
|
590
|
-
const file = await ctx.storage.get("file_id");
|
|
591
|
-
const newId = await ctx.storage.store("bucket", buffer);
|
|
592
|
-
});
|
|
593
|
-
```
|
|
594
|
-
|
|
595
|
-
---
|
|
596
|
-
|
|
597
|
-
## Authentication & Permissions
|
|
598
|
-
|
|
599
|
-
### Auth Modes
|
|
600
|
-
|
|
601
|
-
All functions default to requiring authentication. Override with `auth` option:
|
|
602
|
-
|
|
603
|
-
```ts
|
|
604
|
-
// Required (default) - must be authenticated
|
|
605
|
-
export const privateQuery = query({
|
|
606
|
-
handler: async (ctx) => {
|
|
607
|
-
// ctx.auth.userId is guaranteed to exist
|
|
608
|
-
},
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
// Optional - authentication checked but not required
|
|
612
|
-
export const optionalAuth = query({
|
|
613
|
-
auth: "optional",
|
|
614
|
-
handler: async (ctx) => {
|
|
615
|
-
if (ctx.auth.isAuthenticated) {
|
|
616
|
-
// Authenticated user
|
|
617
|
-
} else {
|
|
618
|
-
// Anonymous user
|
|
619
|
-
}
|
|
620
|
-
},
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
// Public - no authentication required
|
|
624
|
-
export const publicQuery = query({
|
|
60
|
+
export const publicPing = query({
|
|
625
61
|
auth: "public",
|
|
626
|
-
handler: async (
|
|
627
|
-
// Anyone can access
|
|
628
|
-
},
|
|
629
|
-
});
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
### Permissions
|
|
633
|
-
|
|
634
|
-
Add permission checks for role-based access:
|
|
635
|
-
|
|
636
|
-
```ts
|
|
637
|
-
export const adminOnly = query({
|
|
638
|
-
permissions: ["admin"],
|
|
639
|
-
handler: async (ctx) => {
|
|
640
|
-
// Only users with "admin" permission
|
|
641
|
-
},
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
export const moderatorOrAdmin = mutation({
|
|
645
|
-
permissions: ["admin", "moderator"],
|
|
646
|
-
handler: async (ctx) => {
|
|
647
|
-
// Users with either permission
|
|
648
|
-
},
|
|
62
|
+
handler: async () => "pong",
|
|
649
63
|
});
|
|
650
64
|
```
|
|
651
65
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
## Background Jobs
|
|
655
|
-
|
|
656
|
-
Schedule background tasks using the job scheduler.
|
|
66
|
+
## Runtime exports
|
|
657
67
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
preset: SchedulePreset.RunNow,
|
|
666
|
-
payload: { olderThanDays: 30 },
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
// Run in 5 minutes
|
|
670
|
-
await ctx.scheduler.schedule({
|
|
671
|
-
name: "sendReminder",
|
|
672
|
-
preset: SchedulePreset.In5Minutes,
|
|
673
|
-
payload: { userId: ctx.auth.userId },
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
// Recurring with cron
|
|
677
|
-
await ctx.scheduler.schedule({
|
|
678
|
-
name: "dailyReport",
|
|
679
|
-
cron: "0 9 * * *", // Every day at 9 AM
|
|
680
|
-
payload: {},
|
|
681
|
-
});
|
|
682
|
-
});
|
|
683
|
-
```
|
|
68
|
+
Common entry points:
|
|
69
|
+
- `@archlast/server/schema/definition` and `@archlast/server/schema/validators`
|
|
70
|
+
- `@archlast/server/functions/definition` and `@archlast/server/functions/types`
|
|
71
|
+
- `@archlast/server/http` and `@archlast/server/webhook`
|
|
72
|
+
- `@archlast/server/jobs`
|
|
73
|
+
- `@archlast/server/storage/types`
|
|
74
|
+
- `@archlast/server/context`
|
|
684
75
|
|
|
685
|
-
|
|
76
|
+
## Docker templates
|
|
686
77
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
| `Every5Minutes` | Run every 5 minutes |
|
|
692
|
-
| `Every15Minutes` | Run every 15 minutes |
|
|
693
|
-
| `Every30Minutes` | Run every 30 minutes |
|
|
694
|
-
| `Hourly` | Run every hour |
|
|
695
|
-
| `Every6Hours` | Run every 6 hours |
|
|
696
|
-
| `Every12Hours` | Run every 12 hours |
|
|
697
|
-
| `DailyMidnight` | Daily at midnight UTC |
|
|
698
|
-
| `DailyNoon` | Daily at noon UTC |
|
|
699
|
-
| `Weekly` | Weekly on Sunday midnight |
|
|
700
|
-
| `Monthly` | Monthly on the 1st |
|
|
701
|
-
| `In5Minutes` | Once, 5 minutes from now |
|
|
702
|
-
| `In1Hour` | Once, 1 hour from now |
|
|
703
|
-
| `In1Day` | Once, 24 hours from now |
|
|
78
|
+
Templates live under `templates/` in this package:
|
|
79
|
+
- `docker-compose.yml`, `docker-compose.dev.yml`, `docker-compose.prod.yml`
|
|
80
|
+
- `.env.example`
|
|
81
|
+
- `archlast.config.js`
|
|
704
82
|
|
|
705
|
-
|
|
83
|
+
The CLI uses these templates to generate a local Docker setup.
|
|
706
84
|
|
|
707
|
-
##
|
|
708
|
-
|
|
709
|
-
File storage type definitions.
|
|
710
|
-
|
|
711
|
-
```ts
|
|
712
|
-
import type { StorageFile, StorageMetadata } from "@archlast/server/storage/types";
|
|
713
|
-
|
|
714
|
-
interface StorageFile {
|
|
715
|
-
id: string;
|
|
716
|
-
bucket: string;
|
|
717
|
-
filename: string;
|
|
718
|
-
contentType: string;
|
|
719
|
-
size: number;
|
|
720
|
-
createdAt: Date;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
interface StorageMetadata {
|
|
724
|
-
filename: string;
|
|
725
|
-
contentType: string;
|
|
726
|
-
size: number;
|
|
727
|
-
bucket: string;
|
|
728
|
-
createdAt: Date;
|
|
729
|
-
url?: string;
|
|
730
|
-
}
|
|
731
|
-
```
|
|
732
|
-
|
|
733
|
-
---
|
|
734
|
-
|
|
735
|
-
## Subpath Exports
|
|
736
|
-
|
|
737
|
-
Import only what you need:
|
|
738
|
-
|
|
739
|
-
| Import | Description |
|
|
740
|
-
|--------|-------------|
|
|
741
|
-
| `@archlast/server/schema/definition` | `defineSchema`, `defineTable`, relationship helpers |
|
|
742
|
-
| `@archlast/server/schema/validators` | `v` validator object |
|
|
743
|
-
| `@archlast/server/functions/definition` | `query`, `mutation`, `action`, `http`, `webhook`, `rpc` |
|
|
744
|
-
| `@archlast/server/functions/types` | Context types, function types |
|
|
745
|
-
| `@archlast/server/http` | HTTP handler utilities |
|
|
746
|
-
| `@archlast/server/webhook` | Webhook handler utilities |
|
|
747
|
-
| `@archlast/server/jobs` | `SchedulePreset`, job types |
|
|
748
|
-
| `@archlast/server/storage/types` | Storage type definitions |
|
|
749
|
-
| `@archlast/server/context` | Context type exports |
|
|
750
|
-
| `@archlast/server/di/*` | Dependency injection utilities |
|
|
751
|
-
|
|
752
|
-
---
|
|
753
|
-
|
|
754
|
-
## Environment Variables
|
|
85
|
+
## Environment configuration
|
|
755
86
|
|
|
756
87
|
Key variables used by the server runtime:
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
| `AWS_ACCESS_KEY_ID` | - | AWS access key |
|
|
771
|
-
| `AWS_SECRET_ACCESS_KEY` | - | AWS secret key |
|
|
772
|
-
| `ARCHLAST_AUTH_TOKEN_PEPPER` | - | Token signing pepper |
|
|
773
|
-
| `ARCHLAST_DASHBOARD_DIR` | - | Dashboard static files path |
|
|
774
|
-
| `ARCHLAST_DASHBOARD_URL` | - | Dashboard proxy URL |
|
|
775
|
-
| `ARCHLAST_STORE_PORT` | `7001` | Document store port |
|
|
776
|
-
| `ARCHLAST_STORE_HOST` | `127.0.0.1` | Document store host |
|
|
777
|
-
| `ARCHLAST_STORE_NO_TLS` | `false` | Disable TLS for document store |
|
|
778
|
-
|
|
779
|
-
---
|
|
780
|
-
|
|
781
|
-
## Docker Templates
|
|
782
|
-
|
|
783
|
-
Templates for Docker deployment are included in `templates/`:
|
|
784
|
-
|
|
785
|
-
- `docker-compose.yml` - Base compose file
|
|
786
|
-
- `docker-compose.dev.yml` - Development overrides
|
|
787
|
-
- `docker-compose.prod.yml` - Production configuration
|
|
788
|
-
- `.env.example` - Example environment variables
|
|
789
|
-
- `archlast.config.js` - CLI configuration template
|
|
790
|
-
|
|
791
|
-
The CLI uses these templates when running `archlast start`.
|
|
792
|
-
|
|
793
|
-
---
|
|
794
|
-
|
|
795
|
-
## Publishing (Maintainers)
|
|
796
|
-
|
|
797
|
-
See `docs/npm-publishing.md` for release and publish steps.
|
|
798
|
-
|
|
799
|
-
## License
|
|
800
|
-
|
|
801
|
-
MIT
|
|
88
|
+
- `PORT` (default: 4000)
|
|
89
|
+
- `ARCHLAST_DB_ROOT` (default: `./data`)
|
|
90
|
+
- `ARCHLAST_ALLOWED_ORIGINS` (CSV)
|
|
91
|
+
- `ARCHLAST_CORS_ALLOW_CREDENTIALS` (`true` or `false`)
|
|
92
|
+
- `STORAGE_ROOT` and `STORAGE_SIGNING_SECRET`
|
|
93
|
+
- `S3_ENABLED`, `S3_BUCKET`, `S3_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`
|
|
94
|
+
- `ARCHLAST_ADMIN_TOKEN` (deprecated), `ARCHLAST_AUTH_TOKEN_PEPPER`
|
|
95
|
+
- `ARCHLAST_DASHBOARD_DIR` or `ARCHLAST_DASHBOARD_URL`
|
|
96
|
+
- `ARCHLAST_STORE_PORT`, `ARCHLAST_STORE_NO_TLS`
|
|
97
|
+
|
|
98
|
+
## Publishing (maintainers)
|
|
99
|
+
|
|
100
|
+
See `docs/npm-publishing.md` for release and publish steps.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@archlast/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Archlast server library exports and Docker templates",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
"LICENSE"
|
|
151
151
|
],
|
|
152
152
|
"archlast": {
|
|
153
|
-
"dockerImage": "archlast
|
|
153
|
+
"dockerImage": "algochad/archlast-server",
|
|
154
154
|
"defaultTag": "latest",
|
|
155
155
|
"minDockerVersion": "24.0.0"
|
|
156
156
|
},
|