@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.
Files changed (86) hide show
  1. package/.turbo/turbo-build.log +56 -0
  2. package/CHANGELOG.md +32 -0
  3. package/LICENSE +21 -0
  4. package/bin/adminforge.js +317 -0
  5. package/dist/auth-client.cjs +45 -0
  6. package/dist/auth-client.cjs.map +1 -0
  7. package/dist/auth-client.d.cts +17 -0
  8. package/dist/auth-client.d.ts +17 -0
  9. package/dist/auth-client.js +20 -0
  10. package/dist/auth-client.js.map +1 -0
  11. package/dist/auth.cjs +65 -0
  12. package/dist/auth.cjs.map +1 -0
  13. package/dist/auth.d.cts +21 -0
  14. package/dist/auth.d.ts +21 -0
  15. package/dist/auth.js +36 -0
  16. package/dist/auth.js.map +1 -0
  17. package/dist/client-D0cjJVsn.d.ts +20 -0
  18. package/dist/client-sRnmZ-Y9.d.cts +20 -0
  19. package/dist/index-CyzxaE7n.d.cts +124 -0
  20. package/dist/index-CyzxaE7n.d.ts +124 -0
  21. package/dist/index.cjs +453 -0
  22. package/dist/index.cjs.map +1 -0
  23. package/dist/index.d.cts +65 -0
  24. package/dist/index.d.ts +65 -0
  25. package/dist/index.js +410 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/next.cjs +839 -0
  28. package/dist/next.cjs.map +1 -0
  29. package/dist/next.d.cts +84 -0
  30. package/dist/next.d.ts +84 -0
  31. package/dist/next.js +800 -0
  32. package/dist/next.js.map +1 -0
  33. package/dist/styles.css +763 -0
  34. package/dist/styles.css.map +1 -0
  35. package/dist/styles.d.cts +2 -0
  36. package/dist/styles.d.ts +2 -0
  37. package/dist/ui.cjs +2500 -0
  38. package/dist/ui.cjs.map +1 -0
  39. package/dist/ui.d.cts +119 -0
  40. package/dist/ui.d.ts +119 -0
  41. package/dist/ui.js +2448 -0
  42. package/dist/ui.js.map +1 -0
  43. package/eslint.config.js +35 -0
  44. package/package.json +99 -0
  45. package/src/api/controller.ts +234 -0
  46. package/src/api/index.ts +4 -0
  47. package/src/api/next.ts +281 -0
  48. package/src/api/security/agent-auth.ts +134 -0
  49. package/src/auth/config.ts +20 -0
  50. package/src/auth/index.ts +3 -0
  51. package/src/auth/middleware.ts +15 -0
  52. package/src/auth/provider.tsx +28 -0
  53. package/src/core/fields/index.ts +119 -0
  54. package/src/core/hooks/index.ts +60 -0
  55. package/src/core/index.ts +43 -0
  56. package/src/core/registry/index.ts +22 -0
  57. package/src/core/schema/collection.ts +12 -0
  58. package/src/core/schema/config.ts +11 -0
  59. package/src/core/schema/normalize.ts +32 -0
  60. package/src/core/types/index.ts +114 -0
  61. package/src/db/client.ts +146 -0
  62. package/src/db/index.ts +3 -0
  63. package/src/db/schema-generator.ts +104 -0
  64. package/src/fields/index.ts +1 -0
  65. package/src/index.ts +4 -0
  66. package/src/next.ts +3 -0
  67. package/src/styles/adminforge.css +840 -0
  68. package/src/ui/AdminDashboard.tsx +176 -0
  69. package/src/ui/AdminForgeContext.tsx +64 -0
  70. package/src/ui/components/AdminLayout.tsx +107 -0
  71. package/src/ui/form-engine/FormEngine.tsx +250 -0
  72. package/src/ui/form-engine/ImageUpload.tsx +68 -0
  73. package/src/ui/form-engine/RelationInput.tsx +215 -0
  74. package/src/ui/form-engine/RichTextEditor.tsx +708 -0
  75. package/src/ui/index.ts +18 -0
  76. package/src/ui/screens/AdminPage.tsx +162 -0
  77. package/src/ui/screens/AgentTokenPage.tsx +232 -0
  78. package/src/ui/screens/CollectionFormPage.tsx +135 -0
  79. package/src/ui/screens/CollectionListPage.tsx +170 -0
  80. package/src/ui/screens/CollectionSchemaPage.tsx +180 -0
  81. package/src/ui/screens/RoleDetailPage.tsx +147 -0
  82. package/src/ui/screens/RolesListPage.tsx +57 -0
  83. package/src/ui/table-engine/TableEngine.tsx +157 -0
  84. package/src/ui.ts +3 -0
  85. package/tsconfig.json +10 -0
  86. package/tsup.config.ts +54 -0
@@ -0,0 +1,146 @@
1
+ import { PrismaClient } from "@prisma/client";
2
+ import type { AdminForgeConfig, CollectionDefinition, CollectionHooks } from "../core";
3
+ import {
4
+ executeBeforeCreate,
5
+ executeAfterCreate,
6
+ executeBeforeUpdate,
7
+ executeAfterUpdate,
8
+ executeBeforeDelete,
9
+ executeAfterDelete,
10
+ } from "../core";
11
+
12
+ export interface DbClient {
13
+ create(collection: string, data: Record<string, unknown>): Promise<unknown>;
14
+ findMany(collection: string, args?: { where?: Record<string, unknown>; orderBy?: Record<string, string>; skip?: number; take?: number }): Promise<unknown[]>;
15
+ findUnique(collection: string, id: string): Promise<unknown | null>;
16
+ update(collection: string, id: string, data: Record<string, unknown>): Promise<unknown>;
17
+ delete(collection: string, id: string): Promise<unknown>;
18
+ count(collection: string, args?: { where?: Record<string, unknown> }): Promise<number>;
19
+ }
20
+
21
+ export function createDbClient(config: AdminForgeConfig, existingPrisma?: any): DbClient {
22
+ const prisma = existingPrisma || new PrismaClient();
23
+ const collectionMap = new Map<string, CollectionDefinition>();
24
+
25
+ const hookMap = new Map<string, CollectionHooks | undefined>();
26
+
27
+ for (const collection of config.collections) {
28
+ collectionMap.set(collection.name, collection);
29
+ hookMap.set(collection.name, collection.hooks);
30
+ }
31
+
32
+ const getPrismaModel = (collection: string) => {
33
+ const model = (prisma as unknown as Record<string, unknown>)[collection];
34
+ if (!model || typeof model !== "object") {
35
+ throw new Error(`Collection "${collection}" not found in Prisma schema`);
36
+ }
37
+ return model as {
38
+ create: (args: { data: Record<string, unknown>; include?: Record<string, boolean> }) => Promise<unknown>;
39
+ findMany: (args?: Record<string, unknown>) => Promise<unknown[]>;
40
+ findUnique: (args: { where: { id: string }; include?: Record<string, boolean> }) => Promise<unknown | null>;
41
+ update: (args: { where: { id: string }; data: Record<string, unknown>; include?: Record<string, boolean> }) => Promise<unknown>;
42
+ delete: (args: { where: { id: string } }) => Promise<unknown>;
43
+ count: (args?: { where?: Record<string, unknown> }) => Promise<number>;
44
+ };
45
+ };
46
+
47
+ // Build include map for collections with relation fields
48
+ const getIncludeMap = (collectionName: string): Record<string, boolean> | undefined => {
49
+ const collDef = collectionMap.get(collectionName);
50
+ if (!collDef) return undefined;
51
+ const include: Record<string, boolean> = {};
52
+ let hasRelations = false;
53
+ for (const [fieldName, field] of Object.entries(collDef.fields)) {
54
+ if (field.type === "relation") {
55
+ include[fieldName] = true;
56
+ hasRelations = true;
57
+ }
58
+ }
59
+ return hasRelations ? include : undefined;
60
+ };
61
+
62
+ // Transform relation fields from raw IDs/arrays into Prisma connect/set syntax
63
+ const transformRelations = (collectionName: string, data: Record<string, unknown>, isUpdate: boolean): Record<string, unknown> => {
64
+ const collDef = collectionMap.get(collectionName);
65
+ if (!collDef) return data;
66
+ const transformed = { ...data };
67
+ for (const [key, value] of Object.entries(transformed)) {
68
+ const field = collDef.fields[key];
69
+ if (field?.type === "relation") {
70
+ const relationType = field.db.relationType ?? "many-to-one";
71
+ const isMulti = relationType === "many-to-many" || relationType === "one-to-many";
72
+
73
+ if (Array.isArray(value)) {
74
+ // Many-to-many or one-to-many: array of IDs
75
+ const ids = value.filter((id): id is string => typeof id === "string" && id.length > 0);
76
+ if (isUpdate) {
77
+ transformed[key] = { set: ids.map((id) => ({ id })) };
78
+ } else {
79
+ transformed[key] = { connect: ids.map((id) => ({ id })) };
80
+ }
81
+ } else if (!isMulti && typeof value === "string" && value !== "") {
82
+ // Many-to-one: single ID string
83
+ transformed[key] = { connect: { id: value } };
84
+ // Remove the raw field and set the FK field instead for many-to-one
85
+ // Actually Prisma accepts both connect syntax and raw FK, but connect is cleaner
86
+ } else if ((value === null || value === "") && isUpdate) {
87
+ transformed[key] = { disconnect: true };
88
+ } else if (value === null || value === "") {
89
+ delete transformed[key];
90
+ }
91
+ }
92
+ }
93
+ return transformed;
94
+ };
95
+
96
+ return {
97
+ async create(collection: string, data: Record<string, unknown>): Promise<unknown> {
98
+ const hooks = hookMap.get(collection);
99
+ const processedData = await executeBeforeCreate(hooks, data);
100
+ const prismaData = transformRelations(collection, processedData, false);
101
+ const model = getPrismaModel(collection);
102
+ const include = getIncludeMap(collection);
103
+ const result = await model.create({ data: prismaData, ...(include ? { include } : {}) });
104
+ const resultId = (result as { id: string }).id;
105
+ await executeAfterCreate(hooks, processedData, resultId);
106
+ return result;
107
+ },
108
+
109
+ async findMany(collection: string, args: Record<string, unknown> = {}): Promise<unknown[]> {
110
+ const model = getPrismaModel(collection);
111
+ const include = getIncludeMap(collection);
112
+ return model.findMany({ ...args, ...(include ? { include } : {}) });
113
+ },
114
+
115
+ async findUnique(collection: string, id: string): Promise<unknown | null> {
116
+ const model = getPrismaModel(collection);
117
+ const include = getIncludeMap(collection);
118
+ return model.findUnique({ where: { id }, ...(include ? { include } : {}) });
119
+ },
120
+
121
+ async update(collection: string, id: string, data: Record<string, unknown>): Promise<unknown> {
122
+ const hooks = hookMap.get(collection);
123
+ const processedData = await executeBeforeUpdate(hooks, data, id);
124
+ const prismaData = transformRelations(collection, processedData, true);
125
+ const model = getPrismaModel(collection);
126
+ const include = getIncludeMap(collection);
127
+ const result = await model.update({ where: { id }, data: prismaData, ...(include ? { include } : {}) });
128
+ await executeAfterUpdate(hooks, processedData, id);
129
+ return result;
130
+ },
131
+
132
+ async delete(collection: string, id: string): Promise<unknown> {
133
+ const hooks = hookMap.get(collection);
134
+ await executeBeforeDelete(hooks, id);
135
+ const model = getPrismaModel(collection);
136
+ const result = await model.delete({ where: { id } });
137
+ await executeAfterDelete(hooks, id);
138
+ return result;
139
+ },
140
+
141
+ async count(collection: string, args: { where?: Record<string, unknown> } = {}): Promise<number> {
142
+ const model = getPrismaModel(collection);
143
+ return model.count(args);
144
+ },
145
+ };
146
+ }
@@ -0,0 +1,3 @@
1
+ export { generatePrismaSchema } from "./schema-generator.js";
2
+ export { createDbClient } from "./client.js";
3
+ export type { DbClient } from "./client.js";
@@ -0,0 +1,104 @@
1
+ import type { AdminForgeConfig } from "../core";
2
+
3
+ function mapFieldType(dbType: string): string {
4
+ const typeMap: Record<string, string> = {
5
+ String: "String",
6
+ Boolean: "Boolean",
7
+ DateTime: "DateTime",
8
+ Int: "Int",
9
+ Float: "Float",
10
+ Json: "Json",
11
+ };
12
+ return typeMap[dbType] ?? "String";
13
+ }
14
+
15
+ export function generatePrismaSchema(
16
+ config: AdminForgeConfig,
17
+ options?: { provider?: string }
18
+ ): string {
19
+ const provider = options?.provider ?? "sqlite";
20
+ const lines: string[] = [];
21
+ lines.push("generator client {");
22
+ lines.push(' provider = "prisma-client-js"');
23
+ lines.push("}");
24
+ lines.push("");
25
+ lines.push("datasource db {");
26
+ lines.push(` provider = "${provider}"`);
27
+ lines.push(' url = env("DATABASE_URL")');
28
+ lines.push("}");
29
+ lines.push("");
30
+
31
+ const inverseRelations: Record<string, string[]> = {};
32
+
33
+ // First pass: generate fields for each model and collect inverse relations
34
+ const modelBlocks: Record<string, string[]> = {};
35
+
36
+ for (const collection of config.collections) {
37
+ const modelName = collection.name;
38
+ const block: string[] = [];
39
+ block.push(`model ${modelName} {`);
40
+ block.push(" id String @id @default(cuid())");
41
+ block.push(" createdAt DateTime @default(now())");
42
+ block.push(" updatedAt DateTime @updatedAt");
43
+
44
+ for (const [name, field] of Object.entries(collection.fields)) {
45
+ if (field.type === "relation") {
46
+ const targetModel = (field.db.references?.model as string) || (field.ui.props?.to as string) || "String";
47
+ const relType = field.db.relationType || "many-to-one";
48
+
49
+ if (!inverseRelations[targetModel]) inverseRelations[targetModel] = [];
50
+
51
+ if (relType === "many-to-many") {
52
+ block.push(` ${name} ${targetModel}[]`);
53
+ inverseRelations[targetModel].push(` adminforge_inverse_${modelName}_${name} ${modelName}[]`);
54
+ } else if (relType === "one-to-many") {
55
+ block.push(` ${name} ${targetModel}[]`);
56
+ inverseRelations[targetModel].push(` adminforge_inverse_${modelName}_${name}Id String?`);
57
+ inverseRelations[targetModel].push(` adminforge_inverse_${modelName}_${name} ${modelName}? @relation(fields: [adminforge_inverse_${modelName}_${name}Id], references: [id])`);
58
+ } else {
59
+ // many-to-one (default)
60
+ block.push(` ${name}Id String${field.db.nullable ? "?" : ""}`);
61
+ block.push(` ${name} ${targetModel}${field.db.nullable ? "?" : ""} @relation(fields: [${name}Id], references: [id])`);
62
+ inverseRelations[targetModel].push(` adminforge_inverse_${modelName}_${name} ${modelName}[]`);
63
+ }
64
+ } else {
65
+ const prismaType = mapFieldType(field.db.type);
66
+ const nul = field.db.nullable ? "?" : "";
67
+ const annotations: string[] = [];
68
+ if (field.db.unique) annotations.push("@unique");
69
+ if (field.db.default !== undefined) {
70
+ const val = field.db.default;
71
+ if (typeof val === "string") {
72
+ annotations.push(`@default("${val}")`);
73
+ } else if (typeof val === "boolean") {
74
+ annotations.push(`@default(${String(val)})`);
75
+ } else {
76
+ annotations.push(`@default(${String(val)})`);
77
+ }
78
+ }
79
+ const ann = annotations.length > 0 ? ` ${annotations.join(" ")}` : "";
80
+ block.push(` ${name} ${prismaType}${nul}${ann}`);
81
+ }
82
+ }
83
+
84
+ modelBlocks[modelName] = block;
85
+ }
86
+
87
+ // Second pass: append inverse relations and close blocks
88
+ for (const collection of config.collections) {
89
+ const modelName = collection.name;
90
+ const block = modelBlocks[modelName];
91
+ if (inverseRelations[modelName]) {
92
+ block.push("");
93
+ block.push(" // Auto-generated inverse relations");
94
+ for (const inv of inverseRelations[modelName]) {
95
+ block.push(inv);
96
+ }
97
+ }
98
+ block.push("}");
99
+ lines.push(block.join("\n"));
100
+ lines.push("");
101
+ }
102
+
103
+ return lines.join("\n");
104
+ }
@@ -0,0 +1 @@
1
+ export { fields } from "../core";
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./core/index.js";
2
+ export * from "./fields/index.js";
3
+ export * from "./db/index.js";
4
+
package/src/next.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./api/index.js";
2
+ export * from "./api/next.js";
3
+ export * from "./auth/index.js";