@njinlabs/njin 0.10.3 → 0.11.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.
Files changed (55) hide show
  1. package/README.md +48 -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 +79 -0
  12. package/src/core/banner.ts +18 -4
  13. package/src/core/config.ts +58 -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 +46 -26
  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 +607 -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 +579 -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
@@ -0,0 +1,79 @@
1
+ import type z from "zod";
2
+
3
+ // Removes fields marked hideForm: true from a JSON schema's properties/required,
4
+ // recursing into nested object/array shapes so hidden fields never reach the admin panel.
5
+ const stripHiddenFields = (node: any): void => {
6
+ if (!node || typeof node !== "object") return;
7
+
8
+ if (node.properties) {
9
+ for (const [key, prop] of Object.entries(
10
+ node.properties as Record<string, any>,
11
+ )) {
12
+ if (prop?.hideForm) {
13
+ delete node.properties[key];
14
+ if (Array.isArray(node.required)) {
15
+ node.required = node.required.filter((r: string) => r !== key);
16
+ }
17
+ } else {
18
+ stripHiddenFields(prop);
19
+ }
20
+ }
21
+ }
22
+
23
+ if (node.items) stripHiddenFields(node.items);
24
+ };
25
+
26
+ // Shared by both models and vars groups on the /api/schema endpoint — strips
27
+ // non-JSON-representable renderAs types (relation/multi_relation/file) into
28
+ // plain JSON-schema shapes the admin panel can render a form from, and drops
29
+ // any field marked hideForm: true so it never reaches the admin panel.
30
+ //
31
+ // `forAgent` describes every link (relation/file/multi_*) as what it takes on the wire: the
32
+ // related record's id as a plain string. The admin shape for a single relation is an object
33
+ // `{ id }`, which is right for the panel's form but wrong for an agent: a relation written as an
34
+ // object is stored as an embedded copy instead of a link to the record, so it never expands.
35
+ export const toAdminSchema = (
36
+ schema: z.ZodObject,
37
+ options: { forAgent?: boolean } = {},
38
+ ) => {
39
+ const jsonSchema = schema.toJSONSchema({
40
+ unrepresentable: "any",
41
+ override: (ctx) => {
42
+ if (options.forAgent) {
43
+ const kind = ctx.jsonSchema.renderAs;
44
+ const target = String(ctx.jsonSchema.model ?? "");
45
+
46
+ if (kind === "relation" || kind === "file") {
47
+ ctx.jsonSchema.type = "string";
48
+ ctx.jsonSchema.description = `Id of a "${target}" record, as a plain string (not an object).`;
49
+ return;
50
+ }
51
+ if (kind === "multi_relation" || kind === "multi_file") {
52
+ ctx.jsonSchema.type = "array";
53
+ ctx.jsonSchema.items = { type: "string" };
54
+ ctx.jsonSchema.description = `Ids of "${target}" records, as plain strings (not objects).`;
55
+ return;
56
+ }
57
+ }
58
+
59
+ if (ctx.jsonSchema.renderAs === "relation") {
60
+ ctx.jsonSchema.type = "object";
61
+ ctx.jsonSchema.properties = {
62
+ id: {
63
+ type: "string",
64
+ },
65
+ };
66
+ ctx.jsonSchema.required = ["id"];
67
+ ctx.jsonSchema.additionalProperties = {};
68
+ } else if (ctx.jsonSchema.renderAs === "multi_relation") {
69
+ (ctx.jsonSchema.items! as any).type = "string";
70
+ } else if (ctx.jsonSchema.renderAs === "file") {
71
+ ctx.jsonSchema.type = "string";
72
+ }
73
+ },
74
+ });
75
+
76
+ stripHiddenFields(jsonSchema);
77
+
78
+ return jsonSchema;
79
+ };
@@ -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,18 @@ 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;
48
+ mcp?: {
49
+ // Extra OAuth callback URLs an MCP client may register, on top of the built-in ones (Claude,
50
+ // ChatGPT, Cursor, VS Code, and any loopback address). An entry is matched exactly, or as a
51
+ // path prefix when it ends in "*" ("https://app.example.com/oauth/*"). Only add callbacks of
52
+ // clients you trust: it is where the authorization code is sent after a user signs in.
53
+ redirectUris?: string[];
54
+ };
39
55
  // Base directory every project-relative path (src/views, _admin, public, upload dirs)
40
56
  // resolves against. Defaults to process.cwd() — only needs overriding by a build step
41
57
  // (see build-worker.ts) that bakes in an absolute path for a runtime whose cwd isn't
@@ -68,8 +84,15 @@ export type NjinConfig = {
68
84
 
69
85
  export type ResolvedConfig = {
70
86
  port: number;
87
+ publicUrl?: string;
88
+ mcp: { redirectUris: string[] };
71
89
  rootDir: string;
72
- db: { path: string; namespace: string; database: string; auth?: { username: string; password: string } | string };
90
+ db: {
91
+ path: string;
92
+ namespace: string;
93
+ database: string;
94
+ auth?: { username: string; password: string } | string;
95
+ };
73
96
  img: { hosts: string[] };
74
97
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
98
  adapters: { file: FileAdapter<any> };
@@ -133,6 +156,8 @@ export const loadConfig = async (preloaded?: NjinConfig): Promise<void> => {
133
156
  // prefix fails the same way it already does today — naturally, at runtime.
134
157
  resolved = {
135
158
  port: userConfig.port ?? 3000,
159
+ publicUrl: userConfig.publicUrl,
160
+ mcp: { redirectUris: userConfig.mcp?.redirectUris ?? [] },
136
161
  rootDir: userConfig.rootDir ?? process.cwd(),
137
162
  db: {
138
163
  path: userConfig.db?.path ?? "rocksdb://data",
@@ -142,17 +167,39 @@ export const loadConfig = async (preloaded?: NjinConfig): Promise<void> => {
142
167
  },
143
168
  img: { hosts: userConfig.img?.hosts ?? [] },
144
169
  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 ?? [])],
170
+ models: [
171
+ ...plugins.flatMap((p) => p.models ?? []),
172
+ ...(userConfig.models ?? []),
173
+ ],
174
+ hooks: [
175
+ ...plugins.flatMap((p) => p.hooks ?? []),
176
+ ...(userConfig.hooks ?? []),
177
+ ],
178
+ events: [
179
+ ...plugins.flatMap((p) => p.events ?? []),
180
+ ...(userConfig.events ?? []),
181
+ ],
182
+ routes: [
183
+ ...plugins.flatMap((p) => p.routes ?? []),
184
+ ...(userConfig.routes ?? []),
185
+ ],
149
186
  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"),
187
+ helpers: [
188
+ ...plugins.flatMap((p) => p.helpers ?? []),
189
+ ...(userConfig.helpers ?? []),
190
+ ],
191
+ pluginInits: plugins
192
+ .map((p) => p.init)
193
+ .filter(
194
+ (fn): fn is () => Promise<void> | void => typeof fn === "function",
195
+ ),
152
196
  };
153
197
  };
154
198
 
155
199
  export const getConfig = (): ResolvedConfig => {
156
- if (!resolved) throw new Error("Config not loaded yet — loadConfig() must run before getConfig() is called.");
200
+ if (!resolved)
201
+ throw new Error(
202
+ "Config not loaded yet — loadConfig() must run before getConfig() is called.",
203
+ );
157
204
  return resolved;
158
205
  };
@@ -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);order:1}
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();