@adminforge/core 0.3.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/.turbo/turbo-build.log +56 -0
- package/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/bin/adminforge.js +317 -0
- package/dist/auth-client.cjs +45 -0
- package/dist/auth-client.cjs.map +1 -0
- package/dist/auth-client.d.cts +17 -0
- package/dist/auth-client.d.ts +17 -0
- package/dist/auth-client.js +20 -0
- package/dist/auth-client.js.map +1 -0
- package/dist/auth.cjs +65 -0
- package/dist/auth.cjs.map +1 -0
- package/dist/auth.d.cts +21 -0
- package/dist/auth.d.ts +21 -0
- package/dist/auth.js +36 -0
- package/dist/auth.js.map +1 -0
- package/dist/client-D0cjJVsn.d.ts +20 -0
- package/dist/client-sRnmZ-Y9.d.cts +20 -0
- package/dist/index-CyzxaE7n.d.cts +124 -0
- package/dist/index-CyzxaE7n.d.ts +124 -0
- package/dist/index.cjs +453 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +410 -0
- package/dist/index.js.map +1 -0
- package/dist/next.cjs +839 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +84 -0
- package/dist/next.d.ts +84 -0
- package/dist/next.js +800 -0
- package/dist/next.js.map +1 -0
- package/dist/styles.css +763 -0
- package/dist/styles.css.map +1 -0
- package/dist/styles.d.cts +2 -0
- package/dist/styles.d.ts +2 -0
- package/dist/ui.cjs +2500 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.d.cts +119 -0
- package/dist/ui.d.ts +119 -0
- package/dist/ui.js +2448 -0
- package/dist/ui.js.map +1 -0
- package/eslint.config.js +35 -0
- package/package.json +99 -0
- package/src/api/controller.ts +234 -0
- package/src/api/index.ts +4 -0
- package/src/api/next.ts +281 -0
- package/src/api/security/agent-auth.ts +134 -0
- package/src/auth/config.ts +20 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/middleware.ts +15 -0
- package/src/auth/provider.tsx +28 -0
- package/src/core/fields/index.ts +119 -0
- package/src/core/hooks/index.ts +60 -0
- package/src/core/index.ts +43 -0
- package/src/core/registry/index.ts +22 -0
- package/src/core/schema/collection.ts +12 -0
- package/src/core/schema/config.ts +11 -0
- package/src/core/schema/normalize.ts +32 -0
- package/src/core/types/index.ts +114 -0
- package/src/db/client.ts +146 -0
- package/src/db/index.ts +3 -0
- package/src/db/schema-generator.ts +104 -0
- package/src/fields/index.ts +1 -0
- package/src/index.ts +4 -0
- package/src/next.ts +3 -0
- package/src/styles/adminforge.css +840 -0
- package/src/ui/AdminDashboard.tsx +176 -0
- package/src/ui/AdminForgeContext.tsx +64 -0
- package/src/ui/components/AdminLayout.tsx +107 -0
- package/src/ui/form-engine/FormEngine.tsx +250 -0
- package/src/ui/form-engine/ImageUpload.tsx +68 -0
- package/src/ui/form-engine/RelationInput.tsx +215 -0
- package/src/ui/form-engine/RichTextEditor.tsx +708 -0
- package/src/ui/index.ts +18 -0
- package/src/ui/screens/AdminPage.tsx +162 -0
- package/src/ui/screens/AgentTokenPage.tsx +232 -0
- package/src/ui/screens/CollectionFormPage.tsx +135 -0
- package/src/ui/screens/CollectionListPage.tsx +170 -0
- package/src/ui/screens/CollectionSchemaPage.tsx +180 -0
- package/src/ui/screens/RoleDetailPage.tsx +147 -0
- package/src/ui/screens/RolesListPage.tsx +57 -0
- package/src/ui/table-engine/TableEngine.tsx +157 -0
- package/src/ui.ts +3 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +54 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* --- Security Model Definitions ---
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type Action = "create" | "read" | "update" | "delete";
|
|
9
|
+
|
|
10
|
+
export type AgentTokenPayload = {
|
|
11
|
+
sub: string; // userId
|
|
12
|
+
role: string;
|
|
13
|
+
scope: string[]; // format: "collection:action"
|
|
14
|
+
iat: number;
|
|
15
|
+
exp: number;
|
|
16
|
+
iss: "adminforge";
|
|
17
|
+
aud: "agent";
|
|
18
|
+
sessionId: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type AgentSession = AgentTokenPayload;
|
|
22
|
+
|
|
23
|
+
export type SecurityContext = {
|
|
24
|
+
user?: any; // Replace with User type from your auth package
|
|
25
|
+
agent?: AgentSession;
|
|
26
|
+
source: "user" | "agent";
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function getSecret(): string {
|
|
30
|
+
const secret = process.env.ADMINFORGE_SECRET;
|
|
31
|
+
if (!secret) {
|
|
32
|
+
throw new Error("ADMINFORGE_SECRET env var is required. Generate one with: openssl rand -hex 32");
|
|
33
|
+
}
|
|
34
|
+
return secret;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* --- Utilities ---
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
function normalizeScope(scope: string): string {
|
|
42
|
+
const normalized = scope.trim().toLowerCase();
|
|
43
|
+
if (!/^[a-z0-9_-]+:[a-z]+$/.test(normalized)) {
|
|
44
|
+
throw new Error(`Malformed scope format: ${scope}`);
|
|
45
|
+
}
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function assertValidAction(action: string): Action {
|
|
50
|
+
const validActions: Action[] = ["create", "read", "update", "delete"];
|
|
51
|
+
if (!validActions.includes(action as Action)) {
|
|
52
|
+
throw new Error(`Invalid action: ${action}`);
|
|
53
|
+
}
|
|
54
|
+
return action as Action;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function assertValidCollection(collection: string): string {
|
|
58
|
+
if (!/^[a-z0-9_-]+$/.test(collection)) {
|
|
59
|
+
throw new Error(`Invalid collection name: ${collection}`);
|
|
60
|
+
}
|
|
61
|
+
return collection;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* --- Token Lifecycle ---
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
export function generateAgentToken(
|
|
69
|
+
userId: string,
|
|
70
|
+
role: string,
|
|
71
|
+
scopes: string[],
|
|
72
|
+
expiresInSeconds: number = 600
|
|
73
|
+
): string {
|
|
74
|
+
const normalizedScopes = scopes.map(normalizeScope);
|
|
75
|
+
|
|
76
|
+
return jwt.sign(
|
|
77
|
+
{
|
|
78
|
+
sub: userId,
|
|
79
|
+
role,
|
|
80
|
+
scope: normalizedScopes,
|
|
81
|
+
sessionId: crypto.randomUUID(),
|
|
82
|
+
},
|
|
83
|
+
getSecret(),
|
|
84
|
+
{
|
|
85
|
+
expiresIn: expiresInSeconds,
|
|
86
|
+
issuer: "adminforge",
|
|
87
|
+
audience: "agent",
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* --- Verification & Enforcement ---
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
// Stub for revocation capability
|
|
97
|
+
function isRevoked(sessionId: string): boolean {
|
|
98
|
+
// In V1, we return false. V2 will check against a Redis/DB blacklist.
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function verifyAgentToken(token: string): AgentTokenPayload {
|
|
103
|
+
try {
|
|
104
|
+
const payload = jwt.verify(token, getSecret(), {
|
|
105
|
+
issuer: "adminforge",
|
|
106
|
+
audience: "agent",
|
|
107
|
+
}) as unknown as AgentTokenPayload;
|
|
108
|
+
|
|
109
|
+
if (!payload.sub) throw new Error("Missing userId (sub)");
|
|
110
|
+
if (!payload.role) throw new Error("Missing role");
|
|
111
|
+
if (!Array.isArray(payload.scope)) throw new Error("Invalid scope format");
|
|
112
|
+
|
|
113
|
+
if (isRevoked(payload.sessionId)) {
|
|
114
|
+
throw new Error("Session revoked");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Semantic normalization
|
|
118
|
+
payload.scope = payload.scope.map(normalizeScope);
|
|
119
|
+
|
|
120
|
+
return payload;
|
|
121
|
+
} catch (error: any) {
|
|
122
|
+
throw new Error(`Unauthorized: ${error.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function assertScope(agent: AgentSession, collection: string, action: Action): void {
|
|
127
|
+
const validColl = assertValidCollection(collection);
|
|
128
|
+
const validAction = assertValidAction(action);
|
|
129
|
+
const key = `${validColl}:${validAction}`.toLowerCase();
|
|
130
|
+
|
|
131
|
+
if (!agent.scope.includes(key)) {
|
|
132
|
+
throw new Error(`Forbidden: Missing scope ${key}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface AuthConfigOptions {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
secret?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function createAuthConfig(options: AuthConfigOptions) {
|
|
7
|
+
return {
|
|
8
|
+
...options,
|
|
9
|
+
providers: ["credentials"] as const,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const auth = {
|
|
14
|
+
providers: {
|
|
15
|
+
credentials: {
|
|
16
|
+
id: "credentials",
|
|
17
|
+
name: "Credentials",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
} as const;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function adminMiddleware(handler: (request: Request) => Promise<Response>) {
|
|
2
|
+
return async (request: Request): Promise<Response> => {
|
|
3
|
+
const sessionCookie = request.headers.get("cookie") ?? "";
|
|
4
|
+
const hasSession = sessionCookie.includes("next-auth.session-token");
|
|
5
|
+
|
|
6
|
+
if (!hasSession) {
|
|
7
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
8
|
+
status: 401,
|
|
9
|
+
headers: { "Content-Type": "application/json" },
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return handler(request);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
|
|
5
|
+
interface AdminSession {
|
|
6
|
+
user: { id: string; email: string; role?: string } | null;
|
|
7
|
+
role?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const AdminSessionContext = createContext<AdminSession | null>(null);
|
|
11
|
+
|
|
12
|
+
export function AuthProvider({
|
|
13
|
+
children,
|
|
14
|
+
session,
|
|
15
|
+
}: {
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
session: AdminSession | null;
|
|
18
|
+
}) {
|
|
19
|
+
return (
|
|
20
|
+
<AdminSessionContext.Provider value={session}>
|
|
21
|
+
{children}
|
|
22
|
+
</AdminSessionContext.Provider>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useAdminSession(): AdminSession | null {
|
|
27
|
+
return useContext(AdminSessionContext);
|
|
28
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type {
|
|
3
|
+
FieldDefinition,
|
|
4
|
+
FieldMeta,
|
|
5
|
+
FieldOptions,
|
|
6
|
+
TextOptions,
|
|
7
|
+
BooleanOptions,
|
|
8
|
+
RichTextOptions,
|
|
9
|
+
SlugOptions,
|
|
10
|
+
RelationOptions,
|
|
11
|
+
DateOptions,
|
|
12
|
+
ImageOptions,
|
|
13
|
+
} from "../types/index.js";
|
|
14
|
+
|
|
15
|
+
function fieldMeta(options: FieldOptions = {}): FieldMeta {
|
|
16
|
+
return {
|
|
17
|
+
required: Boolean(options.required),
|
|
18
|
+
unique: Boolean(options.unique),
|
|
19
|
+
default: options.default,
|
|
20
|
+
label: options.label,
|
|
21
|
+
hidden: options.hidden,
|
|
22
|
+
readOnly: options.readOnly,
|
|
23
|
+
description: options.description,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function text(options: TextOptions = {}): FieldDefinition {
|
|
28
|
+
return {
|
|
29
|
+
type: "text",
|
|
30
|
+
db: { type: "String", nullable: !options.required, unique: options.unique, default: options.default },
|
|
31
|
+
access: options.access,
|
|
32
|
+
ui: { component: "text", props: { label: options.label, hidden: options.hidden, readOnly: options.readOnly } },
|
|
33
|
+
validation: options.required ? z.string().min(1) : z.string().optional(),
|
|
34
|
+
meta: fieldMeta(options),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function boolean(options: BooleanOptions = {}): FieldDefinition {
|
|
39
|
+
return {
|
|
40
|
+
type: "boolean",
|
|
41
|
+
db: { type: "Boolean", nullable: !options.required, default: options.default ?? false },
|
|
42
|
+
access: options.access,
|
|
43
|
+
ui: { component: "boolean", props: { label: options.label, hidden: options.hidden, readOnly: options.readOnly } },
|
|
44
|
+
validation: options.required ? z.boolean() : z.boolean().optional(),
|
|
45
|
+
meta: fieldMeta(options),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function richText(options: RichTextOptions = {}): FieldDefinition {
|
|
50
|
+
return {
|
|
51
|
+
type: "richText",
|
|
52
|
+
db: { type: "String", nullable: !options.required },
|
|
53
|
+
access: options.access,
|
|
54
|
+
ui: { component: "richText", props: { label: options.label, hidden: options.hidden, readOnly: options.readOnly } },
|
|
55
|
+
validation: options.required ? z.string().min(1) : z.string().optional(),
|
|
56
|
+
meta: fieldMeta(options),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function slug(options: SlugOptions): FieldDefinition {
|
|
61
|
+
return {
|
|
62
|
+
type: "slug",
|
|
63
|
+
db: { type: "String", nullable: !options.required, unique: options.unique ?? true },
|
|
64
|
+
access: options.access,
|
|
65
|
+
ui: { component: "slug", props: { from: options.from, label: options.label, hidden: options.hidden, readOnly: options.readOnly } },
|
|
66
|
+
validation: options.required
|
|
67
|
+
? z.string().regex(/^[a-z0-9-]+$/)
|
|
68
|
+
: z.string().regex(/^[a-z0-9-]+$/).optional(),
|
|
69
|
+
hooks: { beforeSave: (value: unknown) => {
|
|
70
|
+
if (typeof value === "string") return value.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
71
|
+
return value;
|
|
72
|
+
}},
|
|
73
|
+
meta: { ...fieldMeta(options), unique: options.unique ?? true },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function relation(options: RelationOptions): FieldDefinition {
|
|
78
|
+
const isMulti = options.type === "many-to-many" || options.type === "one-to-many";
|
|
79
|
+
return {
|
|
80
|
+
type: "relation",
|
|
81
|
+
db: { type: "String", nullable: !options.required, references: { model: options.to, field: "id" }, relationType: options.type },
|
|
82
|
+
access: options.access,
|
|
83
|
+
ui: { component: "relation", props: { to: options.to, relationType: options.type, label: options.label, hidden: options.hidden, readOnly: options.readOnly } },
|
|
84
|
+
validation: isMulti
|
|
85
|
+
? (options.required ? z.array(z.string()).min(1) : z.array(z.string()).optional())
|
|
86
|
+
: (options.required ? z.string().min(1) : z.string().optional()),
|
|
87
|
+
meta: fieldMeta(options),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function date(options: DateOptions = {}): FieldDefinition {
|
|
92
|
+
return {
|
|
93
|
+
type: "date",
|
|
94
|
+
db: { type: "DateTime", nullable: !options.required },
|
|
95
|
+
access: options.access,
|
|
96
|
+
ui: { component: "date", props: { label: options.label, hidden: options.hidden, readOnly: options.readOnly } },
|
|
97
|
+
validation: options.required ? z.string().datetime() : z.string().datetime().optional(),
|
|
98
|
+
hooks: { beforeSave: (value: unknown) => {
|
|
99
|
+
if (options.autoCreate && !value) return new Date().toISOString();
|
|
100
|
+
return value;
|
|
101
|
+
}},
|
|
102
|
+
meta: fieldMeta(options),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function image(options: ImageOptions = {}): FieldDefinition {
|
|
107
|
+
return {
|
|
108
|
+
type: "image",
|
|
109
|
+
db: { type: "String", nullable: true },
|
|
110
|
+
access: options.access,
|
|
111
|
+
ui: { component: "image", props: { label: options.label, hidden: options.hidden, readOnly: options.readOnly } },
|
|
112
|
+
validation: z.string().optional(),
|
|
113
|
+
meta: fieldMeta(options),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const fields = {
|
|
118
|
+
text, boolean, richText, slug, relation, date, image,
|
|
119
|
+
} as const;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CollectionHooks } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
export async function executeBeforeCreate(
|
|
4
|
+
hooks: CollectionHooks | undefined,
|
|
5
|
+
data: Record<string, unknown>
|
|
6
|
+
): Promise<Record<string, unknown>> {
|
|
7
|
+
if (hooks?.beforeCreate) {
|
|
8
|
+
return await hooks.beforeCreate({ data });
|
|
9
|
+
}
|
|
10
|
+
return data;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function executeAfterCreate(
|
|
14
|
+
hooks: CollectionHooks | undefined,
|
|
15
|
+
data: Record<string, unknown>,
|
|
16
|
+
id: string
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
if (hooks?.afterCreate) {
|
|
19
|
+
await hooks.afterCreate({ data, id });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function executeBeforeUpdate(
|
|
24
|
+
hooks: CollectionHooks | undefined,
|
|
25
|
+
data: Record<string, unknown>,
|
|
26
|
+
id: string
|
|
27
|
+
): Promise<Record<string, unknown>> {
|
|
28
|
+
if (hooks?.beforeUpdate) {
|
|
29
|
+
return await hooks.beforeUpdate({ data, id });
|
|
30
|
+
}
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function executeAfterUpdate(
|
|
35
|
+
hooks: CollectionHooks | undefined,
|
|
36
|
+
data: Record<string, unknown>,
|
|
37
|
+
id: string
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
if (hooks?.afterUpdate) {
|
|
40
|
+
await hooks.afterUpdate({ data, id });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function executeBeforeDelete(
|
|
45
|
+
hooks: CollectionHooks | undefined,
|
|
46
|
+
id: string
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
if (hooks?.beforeDelete) {
|
|
49
|
+
await hooks.beforeDelete({ id });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function executeAfterDelete(
|
|
54
|
+
hooks: CollectionHooks | undefined,
|
|
55
|
+
id: string
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
if (hooks?.afterDelete) {
|
|
58
|
+
await hooks.afterDelete({ id });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export { defineConfig } from "./schema/config.js";
|
|
2
|
+
export { collection } from "./schema/collection.js";
|
|
3
|
+
export { normalize, serializeConfig } from "./schema/normalize.js";
|
|
4
|
+
export type { NormalizedSchema } from "./schema/normalize.js";
|
|
5
|
+
export { fields } from "./fields/index.js";
|
|
6
|
+
export { registerField, getField, getRegisteredFields, clearRegistry } from "./registry/index.js";
|
|
7
|
+
export {
|
|
8
|
+
executeBeforeCreate,
|
|
9
|
+
executeAfterCreate,
|
|
10
|
+
executeBeforeUpdate,
|
|
11
|
+
executeAfterUpdate,
|
|
12
|
+
executeBeforeDelete,
|
|
13
|
+
executeAfterDelete,
|
|
14
|
+
} from "./hooks/index.js";
|
|
15
|
+
export type {
|
|
16
|
+
FieldDefinition,
|
|
17
|
+
FieldDBMapping,
|
|
18
|
+
FieldUI,
|
|
19
|
+
FieldHooks,
|
|
20
|
+
FieldMeta,
|
|
21
|
+
CollectionDefinition,
|
|
22
|
+
CollectionHooks,
|
|
23
|
+
AdminForgeConfig,
|
|
24
|
+
AuthConfig,
|
|
25
|
+
AccessConfig,
|
|
26
|
+
RelationType,
|
|
27
|
+
TextOptions,
|
|
28
|
+
BooleanOptions,
|
|
29
|
+
RichTextOptions,
|
|
30
|
+
SlugOptions,
|
|
31
|
+
RelationOptions,
|
|
32
|
+
DateOptions,
|
|
33
|
+
ImageOptions,
|
|
34
|
+
} from "./types/index.js";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Utility type to infer the record structure from a CollectionDefinition.
|
|
38
|
+
*/
|
|
39
|
+
export type InferRecord<T extends { fields: Record<string, any> }> = {
|
|
40
|
+
id: string;
|
|
41
|
+
} & {
|
|
42
|
+
[K in keyof T["fields"]]: any; // We'll refine this in the future with proper field-to-type mapping
|
|
43
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FieldDefinition } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
const fieldRegistry = new Map<string, FieldDefinition>();
|
|
4
|
+
|
|
5
|
+
export function registerField(name: string, definition: FieldDefinition): void {
|
|
6
|
+
if (fieldRegistry.has(name)) {
|
|
7
|
+
throw new Error(`Field "${name}" is already registered`);
|
|
8
|
+
}
|
|
9
|
+
fieldRegistry.set(name, definition);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getField(name: string): FieldDefinition | undefined {
|
|
13
|
+
return fieldRegistry.get(name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getRegisteredFields(): Map<string, FieldDefinition> {
|
|
17
|
+
return new Map(fieldRegistry);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function clearRegistry(): void {
|
|
21
|
+
fieldRegistry.clear();
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CollectionDefinition, CollectionInput } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
export function collection(input: CollectionInput): CollectionDefinition {
|
|
4
|
+
return {
|
|
5
|
+
name: input.name,
|
|
6
|
+
label: input.label ?? input.name.charAt(0).toUpperCase() + input.name.slice(1),
|
|
7
|
+
icon: input.icon,
|
|
8
|
+
fields: input.fields,
|
|
9
|
+
hooks: input.hooks,
|
|
10
|
+
access: input.access,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AdminForgeConfig, CollectionDefinition, AuthConfig } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
export function defineConfig(config: {
|
|
4
|
+
collections: CollectionDefinition[];
|
|
5
|
+
auth?: AuthConfig;
|
|
6
|
+
}): AdminForgeConfig {
|
|
7
|
+
return {
|
|
8
|
+
collections: config.collections,
|
|
9
|
+
auth: config.auth ?? { enabled: false },
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AdminForgeConfig } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
export type NormalizedSchema = AdminForgeConfig;
|
|
4
|
+
|
|
5
|
+
export function normalize(config: AdminForgeConfig): NormalizedSchema {
|
|
6
|
+
return {
|
|
7
|
+
collections: config.collections.map((c) => ({
|
|
8
|
+
...c,
|
|
9
|
+
label: c.label ?? c.name.charAt(0).toUpperCase() + c.name.slice(1),
|
|
10
|
+
})),
|
|
11
|
+
auth: config.auth ?? { enabled: false },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Removes non-serializable parts of the config (like Zod schemas)
|
|
17
|
+
* so it can be passed from Server Components to Client Components.
|
|
18
|
+
*/
|
|
19
|
+
export function serializeConfig(config: AdminForgeConfig): any {
|
|
20
|
+
return {
|
|
21
|
+
...config,
|
|
22
|
+
collections: config.collections.map((c) => ({
|
|
23
|
+
...c,
|
|
24
|
+
fields: Object.fromEntries(
|
|
25
|
+
Object.entries(c.fields).map(([name, field]) => {
|
|
26
|
+
const { validation, ...rest } = field;
|
|
27
|
+
return [name, rest];
|
|
28
|
+
})
|
|
29
|
+
),
|
|
30
|
+
})),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ZodSchema } from "zod";
|
|
2
|
+
|
|
3
|
+
export type RelationType = "many-to-one" | "one-to-many" | "many-to-many";
|
|
4
|
+
|
|
5
|
+
export interface FieldDBMapping {
|
|
6
|
+
type: string;
|
|
7
|
+
nullable?: boolean;
|
|
8
|
+
unique?: boolean;
|
|
9
|
+
default?: unknown;
|
|
10
|
+
references?: { model: string; field: string };
|
|
11
|
+
relationType?: RelationType;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface FieldUI {
|
|
15
|
+
component: string;
|
|
16
|
+
props?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FieldHooks {
|
|
20
|
+
beforeValidate?: (value: unknown) => unknown;
|
|
21
|
+
beforeSave?: (value: unknown) => unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FieldDefinition {
|
|
25
|
+
type: string;
|
|
26
|
+
db: FieldDBMapping;
|
|
27
|
+
ui: FieldUI;
|
|
28
|
+
validation: ZodSchema;
|
|
29
|
+
meta?: FieldMeta;
|
|
30
|
+
hooks?: FieldHooks;
|
|
31
|
+
access?: AccessConfig;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FieldOptions {
|
|
35
|
+
required?: boolean;
|
|
36
|
+
default?: unknown;
|
|
37
|
+
unique?: boolean;
|
|
38
|
+
label?: string;
|
|
39
|
+
hidden?: boolean;
|
|
40
|
+
readOnly?: boolean;
|
|
41
|
+
description?: string;
|
|
42
|
+
access?: AccessConfig;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface FieldMeta {
|
|
46
|
+
required: boolean;
|
|
47
|
+
unique: boolean;
|
|
48
|
+
default?: unknown;
|
|
49
|
+
label?: string;
|
|
50
|
+
hidden?: boolean;
|
|
51
|
+
readOnly?: boolean;
|
|
52
|
+
description?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface TextOptions extends FieldOptions {}
|
|
56
|
+
export interface BooleanOptions extends FieldOptions {}
|
|
57
|
+
export interface RichTextOptions extends FieldOptions {}
|
|
58
|
+
|
|
59
|
+
export interface SlugOptions extends FieldOptions {
|
|
60
|
+
from: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface RelationOptions extends FieldOptions {
|
|
64
|
+
to: string;
|
|
65
|
+
type: RelationType;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface DateOptions extends FieldOptions {
|
|
69
|
+
autoCreate?: boolean;
|
|
70
|
+
autoUpdate?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ImageOptions extends FieldOptions {}
|
|
74
|
+
|
|
75
|
+
export interface AccessConfig {
|
|
76
|
+
read?: string[];
|
|
77
|
+
create?: string[];
|
|
78
|
+
update?: string[];
|
|
79
|
+
delete?: string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface CollectionHooks {
|
|
83
|
+
beforeCreate?: (ctx: { data: Record<string, unknown> }) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
84
|
+
afterCreate?: (ctx: { data: Record<string, unknown>; id: string }) => void | Promise<void>;
|
|
85
|
+
beforeUpdate?: (ctx: { data: Record<string, unknown>; id: string }) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
86
|
+
afterUpdate?: (ctx: { data: Record<string, unknown>; id: string }) => void | Promise<void>;
|
|
87
|
+
beforeDelete?: (ctx: { id: string }) => void | Promise<void>;
|
|
88
|
+
afterDelete?: (ctx: { id: string }) => void | Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface CollectionDefinition {
|
|
92
|
+
name: string;
|
|
93
|
+
label: string;
|
|
94
|
+
icon?: string;
|
|
95
|
+
fields: Record<string, FieldDefinition>;
|
|
96
|
+
hooks?: CollectionHooks;
|
|
97
|
+
access?: AccessConfig;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface AuthConfig {
|
|
101
|
+
enabled: boolean;
|
|
102
|
+
provider?: "credentials";
|
|
103
|
+
roles?: Record<string, { label?: string; parent?: string }>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface AdminForgeConfig {
|
|
107
|
+
collections: CollectionDefinition[];
|
|
108
|
+
auth?: AuthConfig;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type CollectionInput = Omit<CollectionDefinition, "fields"> & {
|
|
112
|
+
fields: Record<string, FieldDefinition>;
|
|
113
|
+
hooks?: CollectionHooks;
|
|
114
|
+
};
|