@njinlabs/njin 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -1
- package/package.json +3 -2
- package/src/cli/build.ts +84 -48
- package/src/config/module.ts +11 -1
- package/src/core/adapters/bun_filesystem.ts +2 -1
- package/src/core/adapters/s3.ts +2 -1
- package/src/core/config.ts +54 -2
- package/src/core/event.ts +45 -0
- package/src/core/model/hooks.ts +143 -0
- package/src/core/model/index.ts +44 -11
- package/src/core/path_guard.ts +16 -2
- package/src/core/plugin.ts +13 -0
- package/src/core/route.ts +6 -0
- package/src/core/vars/index.ts +64 -0
- package/src/modules/api.ts +57 -20
- package/src/modules/img.ts +141 -137
- package/src/modules/plugin.ts +17 -0
- package/src/modules/surreal.ts +38 -13
- package/src/modules/vars.ts +34 -0
- package/src/modules/view.ts +9 -4
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
export type HookEvent =
|
|
2
|
+
| "beforeCreate"
|
|
3
|
+
| "afterCreate"
|
|
4
|
+
| "beforeUpdate"
|
|
5
|
+
| "afterUpdate"
|
|
6
|
+
| "beforeDestroy"
|
|
7
|
+
| "afterDestroy"
|
|
8
|
+
| "beforeVarsUpdate"
|
|
9
|
+
| "afterVarsUpdate";
|
|
10
|
+
|
|
11
|
+
// Minimal structural shape needed from a model instance to infer hook types —
|
|
12
|
+
// deliberately not importing makeModel's return type here to avoid a circular
|
|
13
|
+
// import with ./index.ts (which imports this file).
|
|
14
|
+
type AnyModel = {
|
|
15
|
+
prefix: string;
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
create: (...args: any[]) => Promise<any>;
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
update: (...args: any[]) => Promise<any>;
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
destroy: (...args: any[]) => Promise<any>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type RecordOf<M extends AnyModel> = Awaited<ReturnType<M["create"]>>;
|
|
25
|
+
type CreateDataOf<M extends AnyModel> = Parameters<M["create"]>[0];
|
|
26
|
+
type UpdateDataOf<M extends AnyModel> = Parameters<M["update"]>[1];
|
|
27
|
+
|
|
28
|
+
// Minimal structural shape needed from a vars group (makeVars, ../vars/index.ts) —
|
|
29
|
+
// singleton settings object, no create/destroy, and update(data) takes a single
|
|
30
|
+
// argument (no id, unlike a model's update(id, data): there's only ever one record
|
|
31
|
+
// per vars group, keyed by its own prefix).
|
|
32
|
+
type AnyVars = {
|
|
33
|
+
prefix: string;
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
update: (...args: any[]) => Promise<any>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type VarsRecordOf<V extends AnyVars> = Awaited<ReturnType<V["update"]>>;
|
|
39
|
+
type VarsUpdateDataOf<V extends AnyVars> = Parameters<V["update"]>[0];
|
|
40
|
+
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
type HookHandler = (...args: any[]) => unknown;
|
|
43
|
+
|
|
44
|
+
const registry = new Map<string, Map<HookEvent, HookHandler[]>>();
|
|
45
|
+
|
|
46
|
+
const register = (event: HookEvent, prefix: string, fn: HookHandler) => {
|
|
47
|
+
let byEvent = registry.get(prefix);
|
|
48
|
+
if (!byEvent) {
|
|
49
|
+
byEvent = new Map();
|
|
50
|
+
registry.set(prefix, byEvent);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const handlers = byEvent.get(event);
|
|
54
|
+
if (handlers) {
|
|
55
|
+
handlers.push(fn);
|
|
56
|
+
} else {
|
|
57
|
+
byEvent.set(event, [fn]);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const beforeCreate = <M extends AnyModel>(
|
|
62
|
+
model: M,
|
|
63
|
+
fn: (
|
|
64
|
+
data: Partial<CreateDataOf<M>>,
|
|
65
|
+
) => void | Partial<CreateDataOf<M>> | Promise<void | Partial<CreateDataOf<M>>>,
|
|
66
|
+
): void => register("beforeCreate", model.prefix, fn);
|
|
67
|
+
|
|
68
|
+
export const afterCreate = <M extends AnyModel>(model: M, fn: (record: RecordOf<M>) => void | Promise<void>): void =>
|
|
69
|
+
register("afterCreate", model.prefix, fn);
|
|
70
|
+
|
|
71
|
+
export const beforeUpdate = <M extends AnyModel>(
|
|
72
|
+
model: M,
|
|
73
|
+
fn: (
|
|
74
|
+
data: Partial<UpdateDataOf<M>>,
|
|
75
|
+
context: { id: string },
|
|
76
|
+
) => void | Partial<UpdateDataOf<M>> | Promise<void | Partial<UpdateDataOf<M>>>,
|
|
77
|
+
): void => register("beforeUpdate", model.prefix, fn);
|
|
78
|
+
|
|
79
|
+
export const afterUpdate = <M extends AnyModel>(model: M, fn: (record: RecordOf<M>) => void | Promise<void>): void =>
|
|
80
|
+
register("afterUpdate", model.prefix, fn);
|
|
81
|
+
|
|
82
|
+
export const beforeDestroy = <M extends AnyModel>(model: M, fn: (id: string) => void | Promise<void>): void =>
|
|
83
|
+
register("beforeDestroy", model.prefix, fn);
|
|
84
|
+
|
|
85
|
+
export const afterDestroy = <M extends AnyModel>(model: M, fn: (record: RecordOf<M>) => void | Promise<void>): void =>
|
|
86
|
+
register("afterDestroy", model.prefix, fn);
|
|
87
|
+
|
|
88
|
+
export const beforeVarsUpdate = <V extends AnyVars>(
|
|
89
|
+
vars: V,
|
|
90
|
+
fn: (
|
|
91
|
+
data: Partial<VarsUpdateDataOf<V>>,
|
|
92
|
+
) => void | Partial<VarsUpdateDataOf<V>> | Promise<void | Partial<VarsUpdateDataOf<V>>>,
|
|
93
|
+
): void => register("beforeVarsUpdate", vars.prefix, fn);
|
|
94
|
+
|
|
95
|
+
export const afterVarsUpdate = <V extends AnyVars>(
|
|
96
|
+
vars: V,
|
|
97
|
+
fn: (record: VarsRecordOf<V>) => void | Promise<void>,
|
|
98
|
+
): void => register("afterVarsUpdate", vars.prefix, fn);
|
|
99
|
+
|
|
100
|
+
// Internal dispatch used by makeModel — runs handlers sequentially (await each),
|
|
101
|
+
// merges before*-handler return values into data, and lets errors propagate so a
|
|
102
|
+
// throwing hook aborts the operation (same contract as UniqueConstraintError).
|
|
103
|
+
export const runBeforeHooks = async <T extends Record<string, unknown>>(
|
|
104
|
+
event: "beforeCreate" | "beforeUpdate" | "beforeVarsUpdate",
|
|
105
|
+
prefix: string,
|
|
106
|
+
data: Partial<T>,
|
|
107
|
+
context: { id?: string },
|
|
108
|
+
): Promise<Partial<T>> => {
|
|
109
|
+
const handlers = registry.get(prefix)?.get(event) as
|
|
110
|
+
| ((data: Partial<T>, context: { id?: string }) => void | Partial<T> | Promise<void | Partial<T>>)[]
|
|
111
|
+
| undefined;
|
|
112
|
+
if (!handlers?.length) return data;
|
|
113
|
+
|
|
114
|
+
let current = data;
|
|
115
|
+
for (const handler of handlers) {
|
|
116
|
+
const result = await handler(current, context);
|
|
117
|
+
if (result) current = { ...current, ...result };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return current;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const runAfterHooks = async <T>(
|
|
124
|
+
event: "afterCreate" | "afterUpdate" | "afterDestroy" | "afterVarsUpdate",
|
|
125
|
+
prefix: string,
|
|
126
|
+
record: T,
|
|
127
|
+
): Promise<void> => {
|
|
128
|
+
const handlers = registry.get(prefix)?.get(event) as ((record: T) => void | Promise<void>)[] | undefined;
|
|
129
|
+
if (!handlers?.length) return;
|
|
130
|
+
|
|
131
|
+
for (const handler of handlers) {
|
|
132
|
+
await handler(record);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
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;
|
|
138
|
+
if (!handlers?.length) return;
|
|
139
|
+
|
|
140
|
+
for (const handler of handlers) {
|
|
141
|
+
await handler(id);
|
|
142
|
+
}
|
|
143
|
+
};
|
package/src/core/model/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import surreal from "../../modules/surreal";
|
|
|
2
2
|
import moment from "moment";
|
|
3
3
|
import { RecordId, Table, type Values } from "surrealdb";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { runAfterHooks, runBeforeDestroyHooks, runBeforeHooks } from "./hooks";
|
|
5
6
|
|
|
6
7
|
export type FormMeta = {
|
|
7
8
|
label: string;
|
|
@@ -93,6 +94,13 @@ export const makeModel = <Rules extends z.ZodObject>(
|
|
|
93
94
|
|
|
94
95
|
const uniqueFieldSet = new Set(uniqueFields);
|
|
95
96
|
|
|
97
|
+
// A real Set, not `key in config.schema.shape` — `in` also matches inherited
|
|
98
|
+
// Object.prototype members (e.g. "constructor", "toString"), and JSON.parse can
|
|
99
|
+
// produce those as literal own-properties on a request body/query object, letting
|
|
100
|
+
// them slip past a schema-shape membership check and get interpolated as a field
|
|
101
|
+
// name into the raw query string built below.
|
|
102
|
+
const filterableFieldSet = new Set(Object.keys(config.schema.shape));
|
|
103
|
+
|
|
96
104
|
// field must be a known unique field — prevents arbitrary field injection
|
|
97
105
|
const isDuplicate = async (field: string, value: unknown, excludeId?: string) => {
|
|
98
106
|
if (!uniqueFieldSet.has(field)) return false;
|
|
@@ -118,13 +126,19 @@ export const makeModel = <Rules extends z.ZodObject>(
|
|
|
118
126
|
};
|
|
119
127
|
|
|
120
128
|
const create = async (data: Values<Data>) => {
|
|
121
|
-
await
|
|
129
|
+
const merged = (await runBeforeHooks("beforeCreate", prefix, data as Record<string, unknown>, {})) as Values<Data>;
|
|
130
|
+
|
|
131
|
+
await assertUnique(merged);
|
|
122
132
|
|
|
123
|
-
|
|
133
|
+
const record = (await surreal()
|
|
124
134
|
.create<Data>(table)
|
|
125
|
-
.content({ ...
|
|
135
|
+
.content({ ...merged, createdAt: moment().toISOString(), updatedAt: moment().toISOString() })
|
|
126
136
|
.output("after")
|
|
127
|
-
.then(([data]) => data) as
|
|
137
|
+
.then(([data]) => data)) as Returning;
|
|
138
|
+
|
|
139
|
+
await runAfterHooks("afterCreate", prefix, record);
|
|
140
|
+
|
|
141
|
+
return record;
|
|
128
142
|
};
|
|
129
143
|
|
|
130
144
|
const read = async ({
|
|
@@ -149,7 +163,7 @@ export const makeModel = <Rules extends z.ZodObject>(
|
|
|
149
163
|
if (filters) {
|
|
150
164
|
for (const [key, value] of Object.entries(filters)) {
|
|
151
165
|
// key must exist in schema — prevents arbitrary field injection
|
|
152
|
-
if (!(key
|
|
166
|
+
if (!filterableFieldSet.has(key)) continue;
|
|
153
167
|
|
|
154
168
|
if (typeof value === "string") {
|
|
155
169
|
// Shorthand: filters[field]=value → equality
|
|
@@ -214,18 +228,32 @@ export const makeModel = <Rules extends z.ZodObject>(
|
|
|
214
228
|
};
|
|
215
229
|
|
|
216
230
|
const update = async (id: string, data: Values<Partial<Data>>) => {
|
|
217
|
-
await
|
|
231
|
+
const merged = (await runBeforeHooks("beforeUpdate", prefix, data as Record<string, unknown>, {
|
|
232
|
+
id,
|
|
233
|
+
})) as Values<Partial<Data>>;
|
|
234
|
+
|
|
235
|
+
await assertUnique(merged, id);
|
|
218
236
|
|
|
219
|
-
|
|
237
|
+
const record = (await (surreal()
|
|
220
238
|
.update<Data>(new RecordId(table, id))
|
|
221
239
|
.merge({
|
|
222
|
-
...
|
|
240
|
+
...merged,
|
|
223
241
|
updatedAt: moment().toISOString(),
|
|
224
|
-
}) as unknown as Promise<Returning
|
|
242
|
+
}) as unknown as Promise<Returning>)) as Returning;
|
|
243
|
+
|
|
244
|
+
await runAfterHooks("afterUpdate", prefix, record);
|
|
245
|
+
|
|
246
|
+
return record;
|
|
225
247
|
};
|
|
226
248
|
|
|
227
|
-
const destroy = (id: string) => {
|
|
228
|
-
|
|
249
|
+
const destroy = async (id: string) => {
|
|
250
|
+
await runBeforeDestroyHooks(prefix, id);
|
|
251
|
+
|
|
252
|
+
const record = (await (surreal().delete<Data>(new RecordId(table, id)) as unknown as Promise<Returning>)) as Returning;
|
|
253
|
+
|
|
254
|
+
await runAfterHooks("afterDestroy", prefix, record);
|
|
255
|
+
|
|
256
|
+
return record;
|
|
229
257
|
};
|
|
230
258
|
|
|
231
259
|
return {
|
|
@@ -242,3 +270,8 @@ export const makeModel = <Rules extends z.ZodObject>(
|
|
|
242
270
|
};
|
|
243
271
|
|
|
244
272
|
export * from "./data_type";
|
|
273
|
+
export * from "./hooks";
|
|
274
|
+
export * from "../event";
|
|
275
|
+
export * from "../plugin";
|
|
276
|
+
export * from "../route";
|
|
277
|
+
export * from "../vars";
|
package/src/core/path_guard.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import { join, normalize } from "node:path";
|
|
1
|
+
import { basename, join, normalize, sep } from "node:path";
|
|
2
2
|
|
|
3
3
|
// Resolves `requestedPath` against `baseDir` and rejects anything that escapes it
|
|
4
4
|
// (e.g. "../../etc/passwd") — shared by every module that serves files straight
|
|
5
5
|
// off disk based on a request path (uploads, admin panel SPA, etc).
|
|
6
6
|
export const resolveSafePath = (baseDir: string, requestedPath: string): string | null => {
|
|
7
|
+
// A bare `startsWith` prefix check would let "/app/uploads-evil" pass for a
|
|
8
|
+
// baseDir of "/app/uploads" — anchor the comparison to a directory boundary
|
|
9
|
+
// instead so only baseDir itself (or a path inside it) matches.
|
|
10
|
+
const normalizedBase = normalize(baseDir);
|
|
11
|
+
const boundary = normalizedBase.endsWith(sep) ? normalizedBase : normalizedBase + sep;
|
|
7
12
|
const resolved = normalize(join(baseDir, requestedPath));
|
|
8
|
-
return resolved.startsWith(
|
|
13
|
+
return resolved === normalizedBase || resolved.startsWith(boundary) ? resolved : null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Strips any directory component from a client-supplied filename (e.g. a multipart
|
|
17
|
+
// upload's `file.name`), leaving only the final path segment. Without this, a name
|
|
18
|
+
// like "../../../config.ts" would let an upload adapter write (or an S3 key target)
|
|
19
|
+
// outside the directory/prefix it's meant to be confined to.
|
|
20
|
+
export const sanitizeFileName = (name: string): string => {
|
|
21
|
+
const base = basename(name);
|
|
22
|
+
return base === "" || base === "." || base === ".." ? "file" : base;
|
|
9
23
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EventFactory, HookFactory, ModelFactory, RouteFactory, VarsFactory } from "./config";
|
|
2
|
+
|
|
3
|
+
export type Plugin = {
|
|
4
|
+
models?: ModelFactory[];
|
|
5
|
+
vars?: VarsFactory[];
|
|
6
|
+
hooks?: HookFactory[];
|
|
7
|
+
events?: EventFactory[];
|
|
8
|
+
routes?: RouteFactory[];
|
|
9
|
+
init?: () => Promise<void> | void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Identity function — DX/typing only, same purpose as defineConfig/route().
|
|
13
|
+
export const definePlugin = (plugin: Plugin): Plugin => plugin;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Elysia } from "elysia";
|
|
2
|
+
|
|
3
|
+
// Thin factory over Elysia itself — purely for DX (autocomplete/type-checking),
|
|
4
|
+
// same purpose as defineConfig. Custom route files get the full Elysia API
|
|
5
|
+
// (schema validation, guards, groups, macros, ...) with no wrapping layer.
|
|
6
|
+
export const route = (config?: ConstructorParameters<typeof Elysia>[0]) => new Elysia(config);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import surreal from "../../modules/surreal";
|
|
2
|
+
import moment from "moment";
|
|
3
|
+
import { RecordId, Table, type Values } from "surrealdb";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { runAfterHooks, runBeforeHooks } from "../model/hooks";
|
|
6
|
+
|
|
7
|
+
// One shared physical table for every VARS group — rows are keyed by each
|
|
8
|
+
// group's own prefix as the record id (vars:general, vars:seo, ...), not one
|
|
9
|
+
// table per group. See surreal.ts's ensureTables() for the DEFINE TABLE call.
|
|
10
|
+
const table = new Table("vars");
|
|
11
|
+
|
|
12
|
+
export const makeVars = <Rules extends z.ZodObject>(
|
|
13
|
+
prefix: string,
|
|
14
|
+
config: {
|
|
15
|
+
schema: Rules;
|
|
16
|
+
name: string;
|
|
17
|
+
},
|
|
18
|
+
) => {
|
|
19
|
+
type Data = z.infer<Rules>;
|
|
20
|
+
type Returning = Data & { updatedAt: string | null };
|
|
21
|
+
|
|
22
|
+
const recordId = new RecordId(table, prefix);
|
|
23
|
+
|
|
24
|
+
// schema.parse() fills in every field's .default() for anything missing —
|
|
25
|
+
// including the row never having been saved at all — so get() always
|
|
26
|
+
// returns a complete, valid object. Every VARS field is expected to
|
|
27
|
+
// declare a .default(...) for this reason.
|
|
28
|
+
const get = async (): Promise<Returning> => {
|
|
29
|
+
const record = await surreal().select<Data & { updatedAt?: string }>(recordId);
|
|
30
|
+
const { updatedAt, ...rest } = (record ?? {}) as Record<string, unknown> & { updatedAt?: string };
|
|
31
|
+
return { ...(config.schema.parse(rest) as Data), updatedAt: updatedAt ?? null };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Upsert, not update — the row may not exist yet on first save.
|
|
35
|
+
const update = async (data: Values<Partial<Data>>): Promise<Returning> => {
|
|
36
|
+
const merged = (await runBeforeHooks(
|
|
37
|
+
"beforeVarsUpdate",
|
|
38
|
+
prefix,
|
|
39
|
+
data as Record<string, unknown>,
|
|
40
|
+
{},
|
|
41
|
+
)) as Values<Partial<Data>>;
|
|
42
|
+
|
|
43
|
+
const record = (await surreal()
|
|
44
|
+
.upsert<Data & { updatedAt: string }>(recordId)
|
|
45
|
+
.merge({ ...merged, updatedAt: moment().toISOString() } as Values<Data & { updatedAt: string }>)
|
|
46
|
+
.output("after")) as (Data & { updatedAt: string }) | undefined;
|
|
47
|
+
|
|
48
|
+
const { updatedAt, ...rest } = (record ?? {}) as Record<string, unknown> & { updatedAt?: string };
|
|
49
|
+
const result = { ...(config.schema.parse(rest) as Data), updatedAt: updatedAt ?? null };
|
|
50
|
+
|
|
51
|
+
await runAfterHooks("afterVarsUpdate", prefix, result);
|
|
52
|
+
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
name: config.name,
|
|
58
|
+
prefix,
|
|
59
|
+
table,
|
|
60
|
+
validation: config.schema,
|
|
61
|
+
get,
|
|
62
|
+
update,
|
|
63
|
+
};
|
|
64
|
+
};
|
package/src/modules/api.ts
CHANGED
|
@@ -5,13 +5,49 @@ import z from "zod";
|
|
|
5
5
|
import auth from "./auth";
|
|
6
6
|
import elysia from "./elysia";
|
|
7
7
|
|
|
8
|
+
// Shared by both models and vars groups on the /api/schema endpoint — strips
|
|
9
|
+
// non-JSON-representable renderAs types (relation/multi_relation/file) into
|
|
10
|
+
// plain JSON-schema shapes the admin panel can render a form from.
|
|
11
|
+
const toAdminSchema = (schema: z.ZodObject) =>
|
|
12
|
+
schema.toJSONSchema({
|
|
13
|
+
unrepresentable: "any",
|
|
14
|
+
override: (ctx) => {
|
|
15
|
+
if (ctx.jsonSchema.renderAs === "relation") {
|
|
16
|
+
ctx.jsonSchema.type = "object";
|
|
17
|
+
ctx.jsonSchema.properties = {
|
|
18
|
+
id: {
|
|
19
|
+
type: "string",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
ctx.jsonSchema.required = ["id"];
|
|
23
|
+
ctx.jsonSchema.additionalProperties = {};
|
|
24
|
+
} else if (ctx.jsonSchema.renderAs === "multi_relation") {
|
|
25
|
+
(ctx.jsonSchema.items! as any).type = "string";
|
|
26
|
+
} else if (ctx.jsonSchema.renderAs === "file") {
|
|
27
|
+
ctx.jsonSchema.type = "string";
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
8
32
|
const api = makeModule(() => {
|
|
9
33
|
const fn = () => {};
|
|
10
34
|
|
|
11
35
|
fn.init = async () => {
|
|
12
36
|
const models = getConfig().models;
|
|
37
|
+
const hooks = getConfig().hooks;
|
|
38
|
+
const events = getConfig().events;
|
|
39
|
+
const routes = getConfig().routes;
|
|
40
|
+
const varsGroups = getConfig().vars;
|
|
13
41
|
const authPlugin = await auth();
|
|
14
42
|
|
|
43
|
+
// Trigger each hook file's import side effect (afterCreate()/etc. registration)
|
|
44
|
+
// before any route goes live, so hooks are always registered before the first request.
|
|
45
|
+
for (const promise of hooks) await promise();
|
|
46
|
+
|
|
47
|
+
// Same reasoning as hooks above, for the event bus (src/core/event.ts): drain
|
|
48
|
+
// every listener file's .listen() registration before any route goes live.
|
|
49
|
+
for (const promise of events) await promise();
|
|
50
|
+
|
|
15
51
|
const controller = new Elysia({ prefix: "/api/schema" }).use(authPlugin.plugin).get(
|
|
16
52
|
"/",
|
|
17
53
|
async () => {
|
|
@@ -23,31 +59,27 @@ const api = makeModule(() => {
|
|
|
23
59
|
return {
|
|
24
60
|
name: data.name,
|
|
25
61
|
prefix: data.prefix,
|
|
26
|
-
schema: data.validation
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
ctx.jsonSchema.type = "string";
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
}),
|
|
62
|
+
schema: toAdminSchema(data.validation),
|
|
63
|
+
};
|
|
64
|
+
})(),
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const allVars = await Promise.all(
|
|
69
|
+
varsGroups.map((varsGroup) =>
|
|
70
|
+
(async () => {
|
|
71
|
+
const { default: group } = await varsGroup();
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
name: group.name,
|
|
75
|
+
prefix: group.prefix,
|
|
76
|
+
schema: toAdminSchema(group.validation),
|
|
45
77
|
};
|
|
46
78
|
})(),
|
|
47
79
|
),
|
|
48
80
|
);
|
|
49
81
|
|
|
50
|
-
return { data: allModels };
|
|
82
|
+
return { data: allModels, vars: allVars };
|
|
51
83
|
},
|
|
52
84
|
{
|
|
53
85
|
auth: true,
|
|
@@ -151,6 +183,11 @@ const api = makeModule(() => {
|
|
|
151
183
|
elysia().use(controller);
|
|
152
184
|
}
|
|
153
185
|
|
|
186
|
+
for (const promise of routes) {
|
|
187
|
+
const { default: plugin } = await promise();
|
|
188
|
+
elysia().use(plugin);
|
|
189
|
+
}
|
|
190
|
+
|
|
154
191
|
return {};
|
|
155
192
|
};
|
|
156
193
|
|