@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.
- package/README.md +31 -0
- package/package.json +19 -4
- package/src/cli/build-assets.ts +4 -1
- package/src/cli/build.ts +47 -23
- package/src/cli/create.ts +35 -9
- package/src/cli/index.ts +2 -1
- package/src/cli/update.ts +93 -16
- package/src/config/module.ts +9 -0
- package/src/core/adapters/bun_filesystem.ts +13 -5
- package/src/core/adapters/s3.ts +18 -6
- package/src/core/admin_schema.ts +54 -0
- package/src/core/banner.ts +18 -4
- package/src/core/config.ts +49 -11
- package/src/core/helper.ts +4 -1
- package/src/core/html_page.ts +63 -0
- package/src/core/model/data_type/array.ts +16 -5
- package/src/core/model/data_type/boolean.ts +4 -1
- package/src/core/model/data_type/date.ts +4 -1
- package/src/core/model/data_type/email.ts +4 -1
- package/src/core/model/data_type/file.ts +17 -4
- package/src/core/model/data_type/multi_file.ts +9 -4
- package/src/core/model/data_type/numeric.ts +4 -1
- package/src/core/model/data_type/object.ts +8 -2
- package/src/core/model/data_type/relation.ts +21 -5
- package/src/core/model/data_type/relation_many.ts +24 -6
- package/src/core/model/data_type/richtext.ts +4 -1
- package/src/core/model/data_type/select.ts +17 -3
- package/src/core/model/data_type/text.ts +4 -1
- package/src/core/model/hooks.ts +42 -15
- package/src/core/model/index.ts +101 -37
- package/src/core/path_guard.ts +10 -3
- package/src/core/plugin.ts +8 -1
- package/src/core/public_url.ts +24 -0
- package/src/core/route.ts +2 -1
- package/src/core/vars/index.ts +22 -8
- package/src/core/worker.ts +22 -6
- package/src/models/file.ts +1 -1
- package/src/models/user.ts +1 -1
- package/src/modules/admin.ts +2 -2
- package/src/modules/analytics.ts +60 -25
- package/src/modules/api.ts +67 -95
- package/src/modules/auth.ts +25 -8
- package/src/modules/elysia.ts +1 -1
- package/src/modules/file.ts +50 -33
- package/src/modules/img.ts +33 -14
- package/src/modules/logger.ts +1 -1
- package/src/modules/mcp.ts +437 -0
- package/src/modules/mcp_token.ts +295 -0
- package/src/modules/mcp_upload.ts +313 -0
- package/src/modules/oauth.ts +550 -0
- package/src/modules/setup.ts +13 -5
- package/src/modules/surreal.ts +43 -13
- package/src/modules/users.ts +23 -5
- package/src/modules/vars.ts +1 -1
- package/src/modules/view.ts +88 -54
package/src/core/banner.ts
CHANGED
|
@@ -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 (
|
|
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
|
-
[
|
|
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
|
|
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(
|
|
61
|
+
...rows.map(
|
|
62
|
+
([key, value]) => ` ${c.green}➜${c.reset} ${label(key)} ${value}`,
|
|
63
|
+
),
|
|
50
64
|
"",
|
|
51
65
|
];
|
|
52
66
|
|
package/src/core/config.ts
CHANGED
|
@@ -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<{
|
|
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<{
|
|
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: {
|
|
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: [
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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: [
|
|
151
|
-
|
|
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)
|
|
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
|
};
|
package/src/core/helper.ts
CHANGED
|
@@ -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 => ({
|
|
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("&", "&")
|
|
12
|
+
.replaceAll("<", "<")
|
|
13
|
+
.replaceAll(">", ">")
|
|
14
|
+
.replaceAll('"', """)
|
|
15
|
+
.replaceAll("'", "'");
|
|
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 {
|
|
2
|
+
import type { FormMeta } from "..";
|
|
3
3
|
|
|
4
|
-
export function array<T extends z.ZodTypeAny>(
|
|
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>(
|
|
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(
|
|
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 (
|
|
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>(
|
|
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>(
|
|
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>(
|
|
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
|
|
2
|
+
import fileModule from "../../../modules/file";
|
|
3
|
+
import { type FormMeta, type RelationType, relation } from "..";
|
|
4
4
|
|
|
5
|
-
export function file(
|
|
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>(
|
|
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
|
|
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<
|
|
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>(
|
|
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>(
|
|
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 }>(
|
|
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<
|
|
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 {
|
|
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<
|
|
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>>(
|
|
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<
|
|
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(
|
|
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 {
|
|
3
|
+
import type { FormMeta, makeModel, RelationType } from "..";
|
|
4
4
|
|
|
5
|
-
type Return<Model extends ReturnType<typeof makeModel>> = z.ZodPreprocess<
|
|
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>>(
|
|
9
|
+
export function relationMany<Model extends ReturnType<typeof makeModel>>(
|
|
10
|
+
meta: FormMeta,
|
|
11
|
+
model: Model,
|
|
12
|
+
): Return<Model>;
|
|
8
13
|
|
|
9
|
-
export function relationMany<
|
|
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(
|
|
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 (
|
|
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>(
|
|
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[]>(
|
|
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<
|
|
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(
|
|
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>(
|
|
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();
|
package/src/core/model/hooks.ts
CHANGED
|
@@ -62,34 +62,51 @@ export const beforeCreate = <M extends AnyModel>(
|
|
|
62
62
|
model: M,
|
|
63
63
|
fn: (
|
|
64
64
|
data: Partial<CreateDataOf<M>>,
|
|
65
|
-
) =>
|
|
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>(
|
|
69
|
-
|
|
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
|
-
) =>
|
|
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>(
|
|
80
|
-
|
|
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>(
|
|
83
|
-
|
|
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>(
|
|
86
|
-
|
|
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
|
-
) =>
|
|
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
|
-
| ((
|
|
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
|
|
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 (
|
|
137
|
-
|
|
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) {
|