@njinlabs/njin 0.10.2 → 0.11.0-beta.1

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.
Files changed (55) hide show
  1. package/README.md +31 -0
  2. package/package.json +19 -4
  3. package/src/cli/build-assets.ts +4 -1
  4. package/src/cli/build.ts +47 -23
  5. package/src/cli/create.ts +35 -9
  6. package/src/cli/index.ts +2 -1
  7. package/src/cli/update.ts +93 -16
  8. package/src/config/module.ts +9 -0
  9. package/src/core/adapters/bun_filesystem.ts +13 -5
  10. package/src/core/adapters/s3.ts +18 -6
  11. package/src/core/admin_schema.ts +54 -0
  12. package/src/core/banner.ts +18 -4
  13. package/src/core/config.ts +49 -11
  14. package/src/core/helper.ts +4 -1
  15. package/src/core/html_page.ts +63 -0
  16. package/src/core/model/data_type/array.ts +16 -5
  17. package/src/core/model/data_type/boolean.ts +4 -1
  18. package/src/core/model/data_type/date.ts +4 -1
  19. package/src/core/model/data_type/email.ts +4 -1
  20. package/src/core/model/data_type/file.ts +17 -4
  21. package/src/core/model/data_type/multi_file.ts +9 -4
  22. package/src/core/model/data_type/numeric.ts +4 -1
  23. package/src/core/model/data_type/object.ts +8 -2
  24. package/src/core/model/data_type/relation.ts +21 -5
  25. package/src/core/model/data_type/relation_many.ts +24 -6
  26. package/src/core/model/data_type/richtext.ts +4 -1
  27. package/src/core/model/data_type/select.ts +17 -3
  28. package/src/core/model/data_type/text.ts +4 -1
  29. package/src/core/model/hooks.ts +42 -15
  30. package/src/core/model/index.ts +101 -37
  31. package/src/core/path_guard.ts +10 -3
  32. package/src/core/plugin.ts +8 -1
  33. package/src/core/public_url.ts +24 -0
  34. package/src/core/route.ts +2 -1
  35. package/src/core/vars/index.ts +22 -8
  36. package/src/core/worker.ts +22 -6
  37. package/src/models/file.ts +1 -1
  38. package/src/models/user.ts +1 -1
  39. package/src/modules/admin.ts +2 -2
  40. package/src/modules/analytics.ts +60 -25
  41. package/src/modules/api.ts +67 -95
  42. package/src/modules/auth.ts +25 -8
  43. package/src/modules/elysia.ts +1 -1
  44. package/src/modules/file.ts +50 -33
  45. package/src/modules/img.ts +33 -14
  46. package/src/modules/logger.ts +1 -1
  47. package/src/modules/mcp.ts +437 -0
  48. package/src/modules/mcp_token.ts +295 -0
  49. package/src/modules/mcp_upload.ts +313 -0
  50. package/src/modules/oauth.ts +550 -0
  51. package/src/modules/setup.ts +13 -5
  52. package/src/modules/surreal.ts +43 -13
  53. package/src/modules/users.ts +23 -5
  54. package/src/modules/vars.ts +1 -1
  55. package/src/modules/view.ts +88 -54
@@ -15,7 +15,11 @@ const PKG_VERSION: string = (() => {
15
15
  try {
16
16
  // Safe to use import.meta.dir here — this looks up njin's OWN version,
17
17
  // not anything project-specific (unlike view.ts's views lookup).
18
- return (require(join(import.meta.dir, "../../package.json")) as { version: string }).version;
18
+ return (
19
+ require(join(import.meta.dir, "../../package.json")) as {
20
+ version: string;
21
+ }
22
+ ).version;
19
23
  } catch {
20
24
  return "0.0.0";
21
25
  }
@@ -35,9 +39,17 @@ export const printBanner = ({ mode }: BootSummary): void => {
35
39
 
36
40
  const rows: [string, string][] = [
37
41
  ["Local", `http://localhost:${port}`],
38
- ["Database", `${config.db.path} ${c.dim}(${config.db.namespace}/${config.db.database})${c.reset}`],
42
+ [
43
+ "Database",
44
+ `${config.db.path} ${c.dim}(${config.db.namespace}/${config.db.database})${c.reset}`,
45
+ ],
39
46
  ["Models", `${config.models.length} registered`],
40
- hasAdmin ? ["Admin", `http://localhost:${port}/_admin`] : ["Admin", `${c.dim}not found — drop a built admin panel into /_admin${c.reset}`],
47
+ hasAdmin
48
+ ? ["Admin", `http://localhost:${port}/_admin`]
49
+ : [
50
+ "Admin",
51
+ `${c.dim}not found — drop a built admin panel into /_admin${c.reset}`,
52
+ ],
41
53
  ];
42
54
 
43
55
  const label = (text: string) => `${c.dim}${text.padEnd(9)}${c.reset}`;
@@ -46,7 +58,9 @@ export const printBanner = ({ mode }: BootSummary): void => {
46
58
  "",
47
59
  ` ${c.bold}${c.cyan}njin${c.reset} ${c.dim}v${PKG_VERSION}${c.reset} ${mode === "production" ? c.green : c.yellow}${mode}${c.reset}`,
48
60
  "",
49
- ...rows.map(([key, value]) => ` ${c.green}➜${c.reset} ${label(key)} ${value}`),
61
+ ...rows.map(
62
+ ([key, value]) => ` ${c.green}➜${c.reset} ${label(key)} ${value}`,
63
+ ),
50
64
  "",
51
65
  ];
52
66
 
@@ -1,13 +1,15 @@
1
+ import { join } from "node:path";
1
2
  import type { AnyElysia } from "elysia";
2
3
  import type { FileAdapter } from "../modules/file";
3
- import { join } from "node:path";
4
4
  import bunFilesystemAdapter from "./adapters/bun_filesystem";
5
5
  import type { Helper } from "./helper";
6
6
  import type { makeModel } from "./model";
7
7
  import type { Plugin } from "./plugin";
8
8
  import type { makeVars } from "./vars";
9
9
 
10
- export type ModelFactory = () => Promise<{ default: ReturnType<typeof makeModel> }>;
10
+ export type ModelFactory = () => Promise<{
11
+ default: ReturnType<typeof makeModel>;
12
+ }>;
11
13
 
12
14
  // A hook file's only job is the side effect of calling afterCreate()/etc. at import
13
15
  // time — its module's exports (if any) are irrelevant, unlike ModelFactory.
@@ -27,7 +29,9 @@ export type RouteFactory = () => Promise<{ default: AnyElysia }>;
27
29
 
28
30
  // A vars file's default export is one makeVars() group (e.g. "general", "seo") —
29
31
  // a singleton settings object, not a list of records like a model.
30
- export type VarsFactory = () => Promise<{ default: ReturnType<typeof makeVars> }>;
32
+ export type VarsFactory = () => Promise<{
33
+ default: ReturnType<typeof makeVars>;
34
+ }>;
31
35
 
32
36
  // A helper file's default export is one defineHelper() — a stateless function
33
37
  // registered as an Edge global by its own name, unlike models/vars which are
@@ -36,6 +40,11 @@ export type HelperFactory = () => Promise<{ default: Helper }>;
36
40
 
37
41
  export type NjinConfig = {
38
42
  port?: number;
43
+ // Public base URL of the site as clients reach it (e.g. "https://example.com") — used for
44
+ // the OAuth/MCP discovery documents, which must name the exact host an agent connects to.
45
+ // Optional: falls back to the request's own origin (see core/public_url.ts), which is
46
+ // enough for a single-host setup without a proxy rewriting Host.
47
+ publicUrl?: string;
39
48
  // Base directory every project-relative path (src/views, _admin, public, upload dirs)
40
49
  // resolves against. Defaults to process.cwd() — only needs overriding by a build step
41
50
  // (see build-worker.ts) that bakes in an absolute path for a runtime whose cwd isn't
@@ -68,8 +77,14 @@ export type NjinConfig = {
68
77
 
69
78
  export type ResolvedConfig = {
70
79
  port: number;
80
+ publicUrl?: string;
71
81
  rootDir: string;
72
- db: { path: string; namespace: string; database: string; auth?: { username: string; password: string } | string };
82
+ db: {
83
+ path: string;
84
+ namespace: string;
85
+ database: string;
86
+ auth?: { username: string; password: string } | string;
87
+ };
73
88
  img: { hosts: string[] };
74
89
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
90
  adapters: { file: FileAdapter<any> };
@@ -133,6 +148,7 @@ export const loadConfig = async (preloaded?: NjinConfig): Promise<void> => {
133
148
  // prefix fails the same way it already does today — naturally, at runtime.
134
149
  resolved = {
135
150
  port: userConfig.port ?? 3000,
151
+ publicUrl: userConfig.publicUrl,
136
152
  rootDir: userConfig.rootDir ?? process.cwd(),
137
153
  db: {
138
154
  path: userConfig.db?.path ?? "rocksdb://data",
@@ -142,17 +158,39 @@ export const loadConfig = async (preloaded?: NjinConfig): Promise<void> => {
142
158
  },
143
159
  img: { hosts: userConfig.img?.hosts ?? [] },
144
160
  adapters: { file: userConfig.adapters?.file ?? bunFilesystemAdapter() },
145
- models: [...plugins.flatMap((p) => p.models ?? []), ...(userConfig.models ?? [])],
146
- hooks: [...plugins.flatMap((p) => p.hooks ?? []), ...(userConfig.hooks ?? [])],
147
- events: [...plugins.flatMap((p) => p.events ?? []), ...(userConfig.events ?? [])],
148
- routes: [...plugins.flatMap((p) => p.routes ?? []), ...(userConfig.routes ?? [])],
161
+ models: [
162
+ ...plugins.flatMap((p) => p.models ?? []),
163
+ ...(userConfig.models ?? []),
164
+ ],
165
+ hooks: [
166
+ ...plugins.flatMap((p) => p.hooks ?? []),
167
+ ...(userConfig.hooks ?? []),
168
+ ],
169
+ events: [
170
+ ...plugins.flatMap((p) => p.events ?? []),
171
+ ...(userConfig.events ?? []),
172
+ ],
173
+ routes: [
174
+ ...plugins.flatMap((p) => p.routes ?? []),
175
+ ...(userConfig.routes ?? []),
176
+ ],
149
177
  vars: [...plugins.flatMap((p) => p.vars ?? []), ...(userConfig.vars ?? [])],
150
- helpers: [...plugins.flatMap((p) => p.helpers ?? []), ...(userConfig.helpers ?? [])],
151
- pluginInits: plugins.map((p) => p.init).filter((fn): fn is () => Promise<void> | void => typeof fn === "function"),
178
+ helpers: [
179
+ ...plugins.flatMap((p) => p.helpers ?? []),
180
+ ...(userConfig.helpers ?? []),
181
+ ],
182
+ pluginInits: plugins
183
+ .map((p) => p.init)
184
+ .filter(
185
+ (fn): fn is () => Promise<void> | void => typeof fn === "function",
186
+ ),
152
187
  };
153
188
  };
154
189
 
155
190
  export const getConfig = (): ResolvedConfig => {
156
- if (!resolved) throw new Error("Config not loaded yet — loadConfig() must run before getConfig() is called.");
191
+ if (!resolved)
192
+ throw new Error(
193
+ "Config not loaded yet — loadConfig() must run before getConfig() is called.",
194
+ );
157
195
  return resolved;
158
196
  };
@@ -2,4 +2,7 @@
2
2
  export type Helper = { name: string; fn: (...args: any[]) => unknown };
3
3
 
4
4
  // Identity function — DX/typing only, same purpose as defineConfig/definePlugin.
5
- export const defineHelper = (name: string, fn: Helper["fn"]): Helper => ({ name, fn });
5
+ export const defineHelper = (name: string, fn: Helper["fn"]): Helper => ({
6
+ name,
7
+ fn,
8
+ });
@@ -0,0 +1,63 @@
1
+ // Shared by the small server-rendered pages (OAuth sign-in, MCP file drop) — they live outside
2
+ // the site's own views on purpose, so they work regardless of what theme/templates a project uses.
3
+
4
+ export const noStore = {
5
+ "Cache-Control": "no-store",
6
+ Pragma: "no-cache",
7
+ };
8
+
9
+ export const escapeHtml = (value: string) =>
10
+ value
11
+ .replaceAll("&", "&amp;")
12
+ .replaceAll("<", "&lt;")
13
+ .replaceAll(">", "&gt;")
14
+ .replaceAll('"', "&quot;")
15
+ .replaceAll("'", "&#39;");
16
+
17
+ // A consent page must never be framable (clickjacking the Allow button) or cached.
18
+ const pageHeaders = {
19
+ "Content-Type": "text/html; charset=utf-8",
20
+ "Content-Security-Policy":
21
+ "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'",
22
+ "X-Frame-Options": "DENY",
23
+ "Referrer-Policy": "no-referrer",
24
+ ...noStore,
25
+ };
26
+
27
+ export const page = (title: string, body: string, status = 200) =>
28
+ new Response(
29
+ `<!doctype html>
30
+ <html lang="en">
31
+ <head>
32
+ <meta charset="utf-8">
33
+ <meta name="viewport" content="width=device-width,initial-scale=1">
34
+ <title>${escapeHtml(title)}</title>
35
+ <style>
36
+ :root{color-scheme:light dark;--bg:#f6f6f7;--card:#fff;--fg:#18181b;--muted:#6b7280;--line:#d4d4d8;--accent:#18181b;--accent-fg:#fff;--danger:#b91c1c}
37
+ @media (prefers-color-scheme:dark){:root{--bg:#09090b;--card:#18181b;--fg:#fafafa;--muted:#a1a1aa;--line:#3f3f46;--accent:#fafafa;--accent-fg:#18181b;--danger:#f87171}}
38
+ *{box-sizing:border-box}
39
+ body{margin:0;min-height:100vh;display:grid;place-items:center;padding:16px;background:var(--bg);color:var(--fg);font:16px/1.5 system-ui,sans-serif}
40
+ main{width:100%;max-width:400px;background:var(--card);border:1px solid var(--line);border-radius:12px;padding:24px}
41
+ h1{font-size:20px;margin:0 0 8px}
42
+ p{margin:0 0 16px;color:var(--muted)}
43
+ strong{color:var(--fg)}
44
+ label{display:block;font-size:14px;margin:12px 0 4px}
45
+ input[type=email],input[type=password]{width:100%;padding:10px 12px;border:1px solid var(--line);border-radius:8px;background:transparent;color:inherit;font:inherit}
46
+ .row{display:flex;gap:8px;margin-top:20px}
47
+ button{flex:1;padding:10px 12px;border-radius:8px;border:1px solid var(--line);background:transparent;color:inherit;font:inherit;cursor:pointer}
48
+ button.primary{background:var(--accent);color:var(--accent-fg);border-color:var(--accent)}
49
+ .error{color:var(--danger);font-size:14px;margin:0 0 12px}
50
+ input[type=file]{width:100%;padding:32px 12px;border:2px dashed var(--line);border-radius:8px;background:transparent;color:inherit;font:inherit;cursor:pointer}
51
+ .ok{color:var(--fg)}
52
+ ul{margin:0 0 16px;padding-left:20px;color:var(--muted)}
53
+ .host{font-family:ui-monospace,monospace;font-size:14px;color:var(--fg)}
54
+ </style>
55
+ </head>
56
+ <body>
57
+ <main>
58
+ ${body}
59
+ </main>
60
+ </body>
61
+ </html>`,
62
+ { status, headers: pageHeaders },
63
+ );
@@ -1,14 +1,25 @@
1
1
  import z from "zod";
2
- import { type FormMeta } from "..";
2
+ import type { FormMeta } from "..";
3
3
 
4
- export function array<T extends z.ZodTypeAny>(meta: FormMeta, input: T): z.ZodPreprocess<z.ZodArray<T>>;
4
+ export function array<T extends z.ZodTypeAny>(
5
+ meta: FormMeta,
6
+ input: T,
7
+ ): z.ZodPreprocess<z.ZodArray<T>>;
5
8
 
6
- export function array<T extends z.ZodTypeAny, Z extends z.ZodTypeAny>(meta: FormMeta, input: T, rule: (z: z.ZodArray<T>) => Z): z.ZodPreprocess<Z>;
9
+ export function array<T extends z.ZodTypeAny, Z extends z.ZodTypeAny>(
10
+ meta: FormMeta,
11
+ input: T,
12
+ rule: (z: z.ZodArray<T>) => Z,
13
+ ): z.ZodPreprocess<Z>;
7
14
 
8
- export function array(meta: FormMeta, input: z.ZodTypeAny, rule?: (z: any) => any) {
15
+ export function array(
16
+ meta: FormMeta,
17
+ input: z.ZodTypeAny,
18
+ rule?: (z: any) => any,
19
+ ) {
9
20
  const baseRule = z.preprocess(
10
21
  (value) => {
11
- if (!!value && !Array.isArray(value)) return [value];
22
+ if (value && !Array.isArray(value)) return [value];
12
23
 
13
24
  return value;
14
25
  },
@@ -3,7 +3,10 @@ import type { FormMeta } from "..";
3
3
 
4
4
  export function boolean(meta: FormMeta): z.ZodBoolean;
5
5
 
6
- export function boolean<T extends z.ZodTypeAny>(meta: FormMeta, rule: (z: z.ZodBoolean) => T): T;
6
+ export function boolean<T extends z.ZodTypeAny>(
7
+ meta: FormMeta,
8
+ rule: (z: z.ZodBoolean) => T,
9
+ ): T;
7
10
 
8
11
  export function boolean(meta: FormMeta, rule?: (z: any) => any) {
9
12
  const baseRule = z.boolean();
@@ -4,7 +4,10 @@ import type { FormMeta } from "..";
4
4
 
5
5
  export function date(meta: FormMeta): z.ZodPreprocess<z.ZodString>;
6
6
 
7
- export function date<T extends z.ZodTypeAny>(meta: FormMeta, rule: (z: z.ZodString) => T): z.ZodPreprocess<T>;
7
+ export function date<T extends z.ZodTypeAny>(
8
+ meta: FormMeta,
9
+ rule: (z: z.ZodString) => T,
10
+ ): z.ZodPreprocess<T>;
8
11
 
9
12
  export function date(meta: FormMeta, rule?: (z: any) => any) {
10
13
  const inner = rule ? rule(z.string()) : z.string();
@@ -3,7 +3,10 @@ import type { FormMeta } from "..";
3
3
 
4
4
  export function email(meta: FormMeta): z.ZodPreprocess<z.ZodEmail>;
5
5
 
6
- export function email<T extends z.ZodTypeAny>(meta: FormMeta, rule: (z: z.ZodEmail) => T): z.ZodPreprocess<T>;
6
+ export function email<T extends z.ZodTypeAny>(
7
+ meta: FormMeta,
8
+ rule: (z: z.ZodEmail) => T,
9
+ ): z.ZodPreprocess<T>;
7
10
 
8
11
  export function email(meta: FormMeta, rule?: (z: any) => any) {
9
12
  const inner = rule ? rule(z.email()) : z.email();
@@ -1,10 +1,23 @@
1
- import fileModule from "../../../modules/file";
2
1
  import type z from "zod";
3
- import { relation, type FormMeta, type RelationType } from "..";
2
+ import fileModule from "../../../modules/file";
3
+ import { type FormMeta, type RelationType, relation } from "..";
4
4
 
5
- export function file(meta: FormMeta): RelationType<ReturnType<ReturnType<typeof fileModule>["model"]["validation"]["partial"]>>;
5
+ export function file(
6
+ meta: FormMeta,
7
+ ): RelationType<
8
+ ReturnType<ReturnType<typeof fileModule>["model"]["validation"]["partial"]>
9
+ >;
6
10
 
7
- export function file<T extends z.ZodTypeAny>(meta: FormMeta, rule: (z: RelationType<ReturnType<ReturnType<typeof fileModule>["model"]["validation"]["partial"]>>) => T): T;
11
+ export function file<T extends z.ZodTypeAny>(
12
+ meta: FormMeta,
13
+ rule: (
14
+ z: RelationType<
15
+ ReturnType<
16
+ ReturnType<typeof fileModule>["model"]["validation"]["partial"]
17
+ >
18
+ >,
19
+ ) => T,
20
+ ): T;
8
21
 
9
22
  export function file(meta: FormMeta, rule?: (z: any) => any) {
10
23
  return relation(meta, fileModule().model, rule ? rule : (z) => z).meta({
@@ -1,13 +1,18 @@
1
- import fileModule from "../../../modules/file";
2
1
  import type z from "zod";
3
- import { type FormMeta, type RelationType } from "..";
2
+ import fileModule from "../../../modules/file";
3
+ import type { FormMeta, RelationType } from "..";
4
4
  import { relationMany } from "./relation_many";
5
5
 
6
- type Return = RelationType<ReturnType<ReturnType<typeof fileModule>["model"]["validation"]["partial"]>>;
6
+ type Return = RelationType<
7
+ ReturnType<ReturnType<typeof fileModule>["model"]["validation"]["partial"]>
8
+ >;
7
9
 
8
10
  export function multiFile(meta: FormMeta): z.ZodPreprocess<z.ZodArray<Return>>;
9
11
 
10
- export function multiFile<T extends z.ZodTypeAny>(meta: FormMeta, rule: (z: z.ZodArray<Return>) => T): T;
12
+ export function multiFile<T extends z.ZodTypeAny>(
13
+ meta: FormMeta,
14
+ rule: (z: z.ZodArray<Return>) => T,
15
+ ): T;
11
16
 
12
17
  export function multiFile(meta: FormMeta, rule?: (z: any) => any) {
13
18
  return relationMany(meta, fileModule().model, rule ? rule : (z) => z).meta({
@@ -3,7 +3,10 @@ import type { FormMeta } from "..";
3
3
 
4
4
  export function numeric(meta: FormMeta): z.ZodNumber;
5
5
 
6
- export function numeric<T extends z.ZodTypeAny>(meta: FormMeta, rule: (z: z.ZodNumber) => T): T;
6
+ export function numeric<T extends z.ZodTypeAny>(
7
+ meta: FormMeta,
8
+ rule: (z: z.ZodNumber) => T,
9
+ ): T;
7
10
 
8
11
  export function numeric(meta: FormMeta, rule?: (z: any) => any) {
9
12
  const baseRule = z.number();
@@ -1,9 +1,15 @@
1
1
  import z from "zod";
2
2
  import type { FormMeta } from "..";
3
3
 
4
- export function object<T extends { [x: string]: z.ZodTypeAny }>(meta: FormMeta, shape: T): z.ZodObject<T>;
4
+ export function object<T extends { [x: string]: z.ZodTypeAny }>(
5
+ meta: FormMeta,
6
+ shape: T,
7
+ ): z.ZodObject<T>;
5
8
 
6
- export function object<T extends { [x: string]: z.ZodTypeAny }, Z extends z.ZodTypeAny>(meta: FormMeta, shape: T, rule: (z: z.ZodObject<T>) => Z): Z;
9
+ export function object<
10
+ T extends { [x: string]: z.ZodTypeAny },
11
+ Z extends z.ZodTypeAny,
12
+ >(meta: FormMeta, shape: T, rule: (z: z.ZodObject<T>) => Z): Z;
7
13
 
8
14
  export function object(meta: FormMeta, shape: any, rule?: (z: any) => any) {
9
15
  const baseRule = z.object(shape);
@@ -1,6 +1,6 @@
1
1
  import { RecordId } from "surrealdb";
2
2
  import z from "zod";
3
- import { type FormMeta, type makeModel } from "..";
3
+ import type { FormMeta, makeModel } from "..";
4
4
 
5
5
  export type RelationType<Relation> = z.ZodPreprocess<
6
6
  z.ZodPipe<
@@ -31,13 +31,29 @@ export type RelationType<Relation> = z.ZodPreprocess<
31
31
  >
32
32
  >;
33
33
 
34
- type Relation<Model extends ReturnType<typeof makeModel>> = RelationType<ReturnType<Model["validation"]["partial"]>>;
34
+ type Relation<Model extends ReturnType<typeof makeModel>> = RelationType<
35
+ ReturnType<Model["validation"]["partial"]>
36
+ >;
35
37
 
36
- export function relation<Model extends ReturnType<typeof makeModel>>(meta: FormMeta & { labelKey?: keyof Model["validation"]["shape"] }, model: Model): Relation<Model>;
38
+ export function relation<Model extends ReturnType<typeof makeModel>>(
39
+ meta: FormMeta & { labelKey?: keyof Model["validation"]["shape"] },
40
+ model: Model,
41
+ ): Relation<Model>;
37
42
 
38
- export function relation<T extends z.ZodTypeAny, Model extends ReturnType<typeof makeModel>>(meta: FormMeta & { labelKey?: keyof Model["validation"]["shape"] }, model: Model, rule: (z: Relation<Model>) => T): T;
43
+ export function relation<
44
+ T extends z.ZodTypeAny,
45
+ Model extends ReturnType<typeof makeModel>,
46
+ >(
47
+ meta: FormMeta & { labelKey?: keyof Model["validation"]["shape"] },
48
+ model: Model,
49
+ rule: (z: Relation<Model>) => T,
50
+ ): T;
39
51
 
40
- export function relation(meta: any, model: ReturnType<typeof makeModel>, rule?: (z: any) => any) {
52
+ export function relation(
53
+ meta: any,
54
+ model: ReturnType<typeof makeModel>,
55
+ rule?: (z: any) => any,
56
+ ) {
41
57
  const baseRule = z.preprocess(
42
58
  (value) => {
43
59
  if (typeof value === "string") {
@@ -1,14 +1,32 @@
1
1
  import { RecordId } from "surrealdb";
2
2
  import z from "zod";
3
- import { type FormMeta, type makeModel, type RelationType } from "..";
3
+ import type { FormMeta, makeModel, RelationType } from "..";
4
4
 
5
- type Return<Model extends ReturnType<typeof makeModel>> = z.ZodPreprocess<z.ZodArray<RelationType<ReturnType<Model["validation"]["partial"]>>>>;
5
+ type Return<Model extends ReturnType<typeof makeModel>> = z.ZodPreprocess<
6
+ z.ZodArray<RelationType<ReturnType<Model["validation"]["partial"]>>>
7
+ >;
6
8
 
7
- export function relationMany<Model extends ReturnType<typeof makeModel>>(meta: FormMeta, model: Model): Return<Model>;
9
+ export function relationMany<Model extends ReturnType<typeof makeModel>>(
10
+ meta: FormMeta,
11
+ model: Model,
12
+ ): Return<Model>;
8
13
 
9
- export function relationMany<T extends z.ZodTypeAny, Model extends ReturnType<typeof makeModel>>(meta: FormMeta, model: Model, rule: (z: z.ZodArray<RelationType<ReturnType<Model["validation"]["partial"]>>>) => T): T;
14
+ export function relationMany<
15
+ T extends z.ZodTypeAny,
16
+ Model extends ReturnType<typeof makeModel>,
17
+ >(
18
+ meta: FormMeta,
19
+ model: Model,
20
+ rule: (
21
+ z: z.ZodArray<RelationType<ReturnType<Model["validation"]["partial"]>>>,
22
+ ) => T,
23
+ ): T;
10
24
 
11
- export function relationMany(meta: FormMeta, model: ReturnType<typeof makeModel>, rule?: (z: any) => any) {
25
+ export function relationMany(
26
+ meta: FormMeta,
27
+ model: ReturnType<typeof makeModel>,
28
+ rule?: (z: any) => any,
29
+ ) {
12
30
  const baseRule = z.array(
13
31
  z.preprocess(
14
32
  (value) => {
@@ -41,7 +59,7 @@ export function relationMany(meta: FormMeta, model: ReturnType<typeof makeModel>
41
59
  return z
42
60
  .preprocess(
43
61
  (value) => {
44
- if (!!value && !Array.isArray(value)) return [value];
62
+ if (value && !Array.isArray(value)) return [value];
45
63
 
46
64
  return value;
47
65
  },
@@ -3,7 +3,10 @@ import type { FormMeta } from "..";
3
3
 
4
4
  export function richtext(meta: FormMeta): z.ZodPreprocess<z.ZodString>;
5
5
 
6
- export function richtext<T extends z.ZodTypeAny>(meta: FormMeta, rule: (z: z.ZodString) => T): z.ZodPreprocess<T>;
6
+ export function richtext<T extends z.ZodTypeAny>(
7
+ meta: FormMeta,
8
+ rule: (z: z.ZodString) => T,
9
+ ): z.ZodPreprocess<T>;
7
10
 
8
11
  export function richtext(meta: FormMeta, rule?: (z: any) => any) {
9
12
  const inner = rule ? rule(z.string()) : z.string();
@@ -1,11 +1,25 @@
1
1
  import z from "zod";
2
2
  import type { FormMeta } from "..";
3
3
 
4
- export function select<const T extends readonly string[]>(meta: FormMeta, options: T): z.ZodEnum<{ [x in T[number]]: x }>;
4
+ export function select<const T extends readonly string[]>(
5
+ meta: FormMeta,
6
+ options: T,
7
+ ): z.ZodEnum<{ [x in T[number]]: x }>;
5
8
 
6
- export function select<const T extends readonly string[], Z extends z.ZodTypeAny>(meta: FormMeta, options: T, rule: (z: z.ZodEnum<{ [x in T[number]]: x }>) => Z): Z;
9
+ export function select<
10
+ const T extends readonly string[],
11
+ Z extends z.ZodTypeAny,
12
+ >(
13
+ meta: FormMeta,
14
+ options: T,
15
+ rule: (z: z.ZodEnum<{ [x in T[number]]: x }>) => Z,
16
+ ): Z;
7
17
 
8
- export function select(meta: FormMeta, options: string[], rule?: (z: any) => any) {
18
+ export function select(
19
+ meta: FormMeta,
20
+ options: string[],
21
+ rule?: (z: any) => any,
22
+ ) {
9
23
  const baseRule = z.enum(options);
10
24
 
11
25
  return (rule ? rule(baseRule) : baseRule).meta({
@@ -3,7 +3,10 @@ import type { FormMeta } from "..";
3
3
 
4
4
  export function text(meta: FormMeta): z.ZodPreprocess<z.ZodString>;
5
5
 
6
- export function text<T extends z.ZodTypeAny>(meta: FormMeta, rule: (z: z.ZodString) => T): z.ZodPreprocess<T>;
6
+ export function text<T extends z.ZodTypeAny>(
7
+ meta: FormMeta,
8
+ rule: (z: z.ZodString) => T,
9
+ ): z.ZodPreprocess<T>;
7
10
 
8
11
  export function text(meta: FormMeta, rule?: (z: any) => any) {
9
12
  const inner = rule ? rule(z.string()) : z.string();
@@ -62,34 +62,51 @@ export const beforeCreate = <M extends AnyModel>(
62
62
  model: M,
63
63
  fn: (
64
64
  data: Partial<CreateDataOf<M>>,
65
- ) => void | Partial<CreateDataOf<M>> | Promise<void | Partial<CreateDataOf<M>>>,
65
+ ) =>
66
+ | void
67
+ | Partial<CreateDataOf<M>>
68
+ | Promise<void | Partial<CreateDataOf<M>>>,
66
69
  ): void => register("beforeCreate", model.prefix, fn);
67
70
 
68
- export const afterCreate = <M extends AnyModel>(model: M, fn: (record: RecordOf<M>) => void | Promise<void>): void =>
69
- register("afterCreate", model.prefix, fn);
71
+ export const afterCreate = <M extends AnyModel>(
72
+ model: M,
73
+ fn: (record: RecordOf<M>) => void | Promise<void>,
74
+ ): void => register("afterCreate", model.prefix, fn);
70
75
 
71
76
  export const beforeUpdate = <M extends AnyModel>(
72
77
  model: M,
73
78
  fn: (
74
79
  data: Partial<UpdateDataOf<M>>,
75
80
  context: { id: string },
76
- ) => void | Partial<UpdateDataOf<M>> | Promise<void | Partial<UpdateDataOf<M>>>,
81
+ ) =>
82
+ | void
83
+ | Partial<UpdateDataOf<M>>
84
+ | Promise<void | Partial<UpdateDataOf<M>>>,
77
85
  ): void => register("beforeUpdate", model.prefix, fn);
78
86
 
79
- export const afterUpdate = <M extends AnyModel>(model: M, fn: (record: RecordOf<M>) => void | Promise<void>): void =>
80
- register("afterUpdate", model.prefix, fn);
87
+ export const afterUpdate = <M extends AnyModel>(
88
+ model: M,
89
+ fn: (record: RecordOf<M>) => void | Promise<void>,
90
+ ): void => register("afterUpdate", model.prefix, fn);
81
91
 
82
- export const beforeDestroy = <M extends AnyModel>(model: M, fn: (id: string) => void | Promise<void>): void =>
83
- register("beforeDestroy", model.prefix, fn);
92
+ export const beforeDestroy = <M extends AnyModel>(
93
+ model: M,
94
+ fn: (id: string) => void | Promise<void>,
95
+ ): void => register("beforeDestroy", model.prefix, fn);
84
96
 
85
- export const afterDestroy = <M extends AnyModel>(model: M, fn: (record: RecordOf<M>) => void | Promise<void>): void =>
86
- register("afterDestroy", model.prefix, fn);
97
+ export const afterDestroy = <M extends AnyModel>(
98
+ model: M,
99
+ fn: (record: RecordOf<M>) => void | Promise<void>,
100
+ ): void => register("afterDestroy", model.prefix, fn);
87
101
 
88
102
  export const beforeVarsUpdate = <V extends AnyVars>(
89
103
  vars: V,
90
104
  fn: (
91
105
  data: Partial<VarsUpdateDataOf<V>>,
92
- ) => void | Partial<VarsUpdateDataOf<V>> | Promise<void | Partial<VarsUpdateDataOf<V>>>,
106
+ ) =>
107
+ | void
108
+ | Partial<VarsUpdateDataOf<V>>
109
+ | Promise<void | Partial<VarsUpdateDataOf<V>>>,
93
110
  ): void => register("beforeVarsUpdate", vars.prefix, fn);
94
111
 
95
112
  export const afterVarsUpdate = <V extends AnyVars>(
@@ -107,7 +124,10 @@ export const runBeforeHooks = async <T extends Record<string, unknown>>(
107
124
  context: { id?: string },
108
125
  ): Promise<Partial<T>> => {
109
126
  const handlers = registry.get(prefix)?.get(event) as
110
- | ((data: Partial<T>, context: { id?: string }) => void | Partial<T> | Promise<void | Partial<T>>)[]
127
+ | ((
128
+ data: Partial<T>,
129
+ context: { id?: string },
130
+ ) => void | Partial<T> | Promise<void | Partial<T>>)[]
111
131
  | undefined;
112
132
  if (!handlers?.length) return data;
113
133
 
@@ -125,7 +145,9 @@ export const runAfterHooks = async <T>(
125
145
  prefix: string,
126
146
  record: T,
127
147
  ): Promise<void> => {
128
- const handlers = registry.get(prefix)?.get(event) as ((record: T) => void | Promise<void>)[] | undefined;
148
+ const handlers = registry.get(prefix)?.get(event) as
149
+ | ((record: T) => void | Promise<void>)[]
150
+ | undefined;
129
151
  if (!handlers?.length) return;
130
152
 
131
153
  for (const handler of handlers) {
@@ -133,8 +155,13 @@ export const runAfterHooks = async <T>(
133
155
  }
134
156
  };
135
157
 
136
- export const runBeforeDestroyHooks = async (prefix: string, id: string): Promise<void> => {
137
- const handlers = registry.get(prefix)?.get("beforeDestroy") as ((id: string) => void | Promise<void>)[] | undefined;
158
+ export const runBeforeDestroyHooks = async (
159
+ prefix: string,
160
+ id: string,
161
+ ): Promise<void> => {
162
+ const handlers = registry.get(prefix)?.get("beforeDestroy") as
163
+ | ((id: string) => void | Promise<void>)[]
164
+ | undefined;
138
165
  if (!handlers?.length) return;
139
166
 
140
167
  for (const handler of handlers) {