@cinnabun/admin 0.0.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/dist/admin.module.d.ts +6 -0
- package/dist/admin.module.js +148 -0
- package/dist/admin.plugin.d.ts +5 -0
- package/dist/admin.plugin.js +16 -0
- package/dist/decorators/admin-field.d.ts +14 -0
- package/dist/decorators/admin-field.js +26 -0
- package/dist/decorators/admin-resource.d.ts +15 -0
- package/dist/decorators/admin-resource.js +17 -0
- package/dist/guards/admin-auth.guard.d.ts +5 -0
- package/dist/guards/admin-auth.guard.js +60 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +5 -0
- package/dist/interfaces/admin-field.d.ts +13 -0
- package/dist/interfaces/admin-field.js +1 -0
- package/dist/interfaces/admin-options.d.ts +11 -0
- package/dist/interfaces/admin-options.js +1 -0
- package/dist/interfaces/admin-resource.d.ts +16 -0
- package/dist/interfaces/admin-resource.js +1 -0
- package/dist/metadata/admin-storage.d.ts +9 -0
- package/dist/metadata/admin-storage.js +19 -0
- package/dist/services/admin.service.d.ts +43 -0
- package/dist/services/admin.service.js +81 -0
- package/dist/ui/admin-ui.d.ts +1 -0
- package/dist/ui/admin-ui.js +230 -0
- package/package.json +34 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Module } from "@cinnabun/core";
|
|
2
|
+
import type { AdminModuleOptions } from "./interfaces/admin-options.js";
|
|
3
|
+
export declare class AdminModule {
|
|
4
|
+
static forRoot(options: AdminModuleOptions): ReturnType<typeof Module>;
|
|
5
|
+
static getOptions(): AdminModuleOptions;
|
|
6
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { Module, RestController, GetMapping, PostMapping, PutMapping, DeleteMapping, Body, Param, Query, UseGuard, NotFoundException, } from "@cinnabun/core";
|
|
14
|
+
import { AdminService } from "./services/admin.service.js";
|
|
15
|
+
import { createAdminAuthGuard } from "./guards/admin-auth.guard.js";
|
|
16
|
+
import { buildAdminUIHtml } from "./ui/admin-ui.js";
|
|
17
|
+
let moduleOptions = null;
|
|
18
|
+
export class AdminModule {
|
|
19
|
+
static forRoot(options) {
|
|
20
|
+
moduleOptions = options;
|
|
21
|
+
const basePath = (options.path ?? "/admin").replace(/\/$/, "") || "/admin";
|
|
22
|
+
const authGuard = createAdminAuthGuard(options.auth?.guard, options.auth?.roles);
|
|
23
|
+
let DynamicAdminController = class DynamicAdminController {
|
|
24
|
+
adminService;
|
|
25
|
+
constructor(adminService) {
|
|
26
|
+
this.adminService = adminService;
|
|
27
|
+
}
|
|
28
|
+
getUI() {
|
|
29
|
+
const html = buildAdminUIHtml(options.title ?? "Admin Dashboard", basePath);
|
|
30
|
+
return new Response(html, {
|
|
31
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
getMetadata() {
|
|
35
|
+
return this.adminService.getMetadata();
|
|
36
|
+
}
|
|
37
|
+
getResources() {
|
|
38
|
+
return this.adminService.getResources();
|
|
39
|
+
}
|
|
40
|
+
async list(resource, page, perPage) {
|
|
41
|
+
const p = Math.max(1, parseInt(page ?? "1", 10) || 1);
|
|
42
|
+
const pp = Math.max(1, Math.min(100, parseInt(perPage ?? "25", 10) || 25));
|
|
43
|
+
return this.adminService.list(resource, p, pp);
|
|
44
|
+
}
|
|
45
|
+
async get(resource, id) {
|
|
46
|
+
const numId = /^\d+$/.test(id) ? parseInt(id, 10) : id;
|
|
47
|
+
const entity = await this.adminService.get(resource, numId);
|
|
48
|
+
if (!entity) {
|
|
49
|
+
throw new NotFoundException(`Entity not found: ${id}`);
|
|
50
|
+
}
|
|
51
|
+
return entity;
|
|
52
|
+
}
|
|
53
|
+
async create(resource, body) {
|
|
54
|
+
return this.adminService.create(resource, body);
|
|
55
|
+
}
|
|
56
|
+
async update(resource, id, body) {
|
|
57
|
+
const numId = /^\d+$/.test(id) ? parseInt(id, 10) : id;
|
|
58
|
+
return this.adminService.update(resource, numId, body);
|
|
59
|
+
}
|
|
60
|
+
async delete(resource, id) {
|
|
61
|
+
const numId = /^\d+$/.test(id) ? parseInt(id, 10) : id;
|
|
62
|
+
await this.adminService.delete(resource, numId);
|
|
63
|
+
return { success: true };
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
__decorate([
|
|
67
|
+
GetMapping("/"),
|
|
68
|
+
__metadata("design:type", Function),
|
|
69
|
+
__metadata("design:paramtypes", []),
|
|
70
|
+
__metadata("design:returntype", void 0)
|
|
71
|
+
], DynamicAdminController.prototype, "getUI", null);
|
|
72
|
+
__decorate([
|
|
73
|
+
GetMapping("/api/metadata"),
|
|
74
|
+
__metadata("design:type", Function),
|
|
75
|
+
__metadata("design:paramtypes", []),
|
|
76
|
+
__metadata("design:returntype", void 0)
|
|
77
|
+
], DynamicAdminController.prototype, "getMetadata", null);
|
|
78
|
+
__decorate([
|
|
79
|
+
GetMapping("/api/resources"),
|
|
80
|
+
__metadata("design:type", Function),
|
|
81
|
+
__metadata("design:paramtypes", []),
|
|
82
|
+
__metadata("design:returntype", void 0)
|
|
83
|
+
], DynamicAdminController.prototype, "getResources", null);
|
|
84
|
+
__decorate([
|
|
85
|
+
GetMapping("/api/:resource"),
|
|
86
|
+
__param(0, Param("resource")),
|
|
87
|
+
__param(1, Query("page")),
|
|
88
|
+
__param(2, Query("perPage")),
|
|
89
|
+
__metadata("design:type", Function),
|
|
90
|
+
__metadata("design:paramtypes", [String, String, String]),
|
|
91
|
+
__metadata("design:returntype", Promise)
|
|
92
|
+
], DynamicAdminController.prototype, "list", null);
|
|
93
|
+
__decorate([
|
|
94
|
+
GetMapping("/api/:resource/:id"),
|
|
95
|
+
__param(0, Param("resource")),
|
|
96
|
+
__param(1, Param("id")),
|
|
97
|
+
__metadata("design:type", Function),
|
|
98
|
+
__metadata("design:paramtypes", [String, String]),
|
|
99
|
+
__metadata("design:returntype", Promise)
|
|
100
|
+
], DynamicAdminController.prototype, "get", null);
|
|
101
|
+
__decorate([
|
|
102
|
+
PostMapping("/api/:resource"),
|
|
103
|
+
__param(0, Param("resource")),
|
|
104
|
+
__param(1, Body()),
|
|
105
|
+
__metadata("design:type", Function),
|
|
106
|
+
__metadata("design:paramtypes", [String, Object]),
|
|
107
|
+
__metadata("design:returntype", Promise)
|
|
108
|
+
], DynamicAdminController.prototype, "create", null);
|
|
109
|
+
__decorate([
|
|
110
|
+
PutMapping("/api/:resource/:id"),
|
|
111
|
+
__param(0, Param("resource")),
|
|
112
|
+
__param(1, Param("id")),
|
|
113
|
+
__param(2, Body()),
|
|
114
|
+
__metadata("design:type", Function),
|
|
115
|
+
__metadata("design:paramtypes", [String, String, Object]),
|
|
116
|
+
__metadata("design:returntype", Promise)
|
|
117
|
+
], DynamicAdminController.prototype, "update", null);
|
|
118
|
+
__decorate([
|
|
119
|
+
DeleteMapping("/api/:resource/:id"),
|
|
120
|
+
__param(0, Param("resource")),
|
|
121
|
+
__param(1, Param("id")),
|
|
122
|
+
__metadata("design:type", Function),
|
|
123
|
+
__metadata("design:paramtypes", [String, String]),
|
|
124
|
+
__metadata("design:returntype", Promise)
|
|
125
|
+
], DynamicAdminController.prototype, "delete", null);
|
|
126
|
+
DynamicAdminController = __decorate([
|
|
127
|
+
RestController(basePath),
|
|
128
|
+
UseGuard(authGuard),
|
|
129
|
+
__metadata("design:paramtypes", [AdminService])
|
|
130
|
+
], DynamicAdminController);
|
|
131
|
+
let AdminDynamicModule = class AdminDynamicModule {
|
|
132
|
+
};
|
|
133
|
+
AdminDynamicModule = __decorate([
|
|
134
|
+
Module({
|
|
135
|
+
controllers: [DynamicAdminController],
|
|
136
|
+
providers: [AdminService],
|
|
137
|
+
exports: [AdminService],
|
|
138
|
+
})
|
|
139
|
+
], AdminDynamicModule);
|
|
140
|
+
return AdminDynamicModule;
|
|
141
|
+
}
|
|
142
|
+
static getOptions() {
|
|
143
|
+
if (!moduleOptions) {
|
|
144
|
+
throw new Error("AdminModule not initialized. Call AdminModule.forRoot() first.");
|
|
145
|
+
}
|
|
146
|
+
return moduleOptions;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Logger } from "@cinnabun/core";
|
|
2
|
+
import { AdminService } from "./services/admin.service.js";
|
|
3
|
+
export class AdminPlugin {
|
|
4
|
+
name = "AdminPlugin";
|
|
5
|
+
async onInit(context) {
|
|
6
|
+
const logger = new Logger("AdminPlugin");
|
|
7
|
+
try {
|
|
8
|
+
const adminService = context.container.resolve(AdminService);
|
|
9
|
+
adminService.setContainer(context.container);
|
|
10
|
+
logger.info("Admin panel initialized");
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
logger.warn("AdminPlugin: AdminService not found. Ensure AdminModule.forRoot() is imported.");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AdminField, AdminFieldType } from "../interfaces/admin-field.js";
|
|
2
|
+
export interface AdminFieldOptions {
|
|
3
|
+
type: AdminFieldType;
|
|
4
|
+
label?: string;
|
|
5
|
+
required?: boolean;
|
|
6
|
+
options?: {
|
|
7
|
+
label: string;
|
|
8
|
+
value: unknown;
|
|
9
|
+
}[];
|
|
10
|
+
readonly?: boolean;
|
|
11
|
+
hidden?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function getAdminFields(target: object): AdminField[];
|
|
14
|
+
export declare function AdminField(options: AdminFieldOptions): PropertyDecorator;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const ADMIN_FIELDS_KEY = Symbol("admin:fields");
|
|
2
|
+
export function getAdminFields(target) {
|
|
3
|
+
const fields = target.constructor[ADMIN_FIELDS_KEY];
|
|
4
|
+
if (!fields)
|
|
5
|
+
return [];
|
|
6
|
+
return Array.from(fields.values());
|
|
7
|
+
}
|
|
8
|
+
export function AdminField(options) {
|
|
9
|
+
return (target, propertyKey) => {
|
|
10
|
+
const name = String(propertyKey);
|
|
11
|
+
let fieldsMap = target.constructor[ADMIN_FIELDS_KEY];
|
|
12
|
+
if (!fieldsMap) {
|
|
13
|
+
fieldsMap = new Map();
|
|
14
|
+
target.constructor[ADMIN_FIELDS_KEY] = fieldsMap;
|
|
15
|
+
}
|
|
16
|
+
fieldsMap.set(name, {
|
|
17
|
+
name,
|
|
18
|
+
type: options.type,
|
|
19
|
+
label: options.label,
|
|
20
|
+
required: options.required,
|
|
21
|
+
options: options.options,
|
|
22
|
+
readonly: options.readonly,
|
|
23
|
+
hidden: options.hidden,
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Constructor } from "../interfaces/admin-resource.js";
|
|
2
|
+
export interface AdminResourceOptions {
|
|
3
|
+
name: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
repository: Constructor;
|
|
6
|
+
entity?: Constructor;
|
|
7
|
+
list?: {
|
|
8
|
+
columns?: string[];
|
|
9
|
+
perPage?: number;
|
|
10
|
+
};
|
|
11
|
+
form?: {
|
|
12
|
+
fields?: string[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export declare function AdminResource(options: AdminResourceOptions): ClassDecorator;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { adminMetadataStorage } from "../metadata/admin-storage.js";
|
|
2
|
+
import { getAdminFields } from "./admin-field.js";
|
|
3
|
+
export function AdminResource(options) {
|
|
4
|
+
return (target) => {
|
|
5
|
+
const fields = getAdminFields(target.prototype);
|
|
6
|
+
const resource = {
|
|
7
|
+
name: options.name,
|
|
8
|
+
label: options.label,
|
|
9
|
+
entity: options.entity ?? target,
|
|
10
|
+
repository: options.repository,
|
|
11
|
+
fields: fields.length > 0 ? fields : [],
|
|
12
|
+
list: options.list,
|
|
13
|
+
form: options.form,
|
|
14
|
+
};
|
|
15
|
+
adminMetadataStorage.addResource(resource);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Guard } from "@cinnabun/core";
|
|
2
|
+
import type { Constructor } from "../interfaces/admin-resource.js";
|
|
3
|
+
type GuardConstructor = Constructor<Guard>;
|
|
4
|
+
export declare function createAdminAuthGuard(guardClass?: GuardConstructor, roles?: string[]): GuardConstructor;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ForbiddenException } from "@cinnabun/core";
|
|
2
|
+
export function createAdminAuthGuard(guardClass, roles) {
|
|
3
|
+
if (!guardClass && !roles?.length) {
|
|
4
|
+
return class NoOpGuard {
|
|
5
|
+
canActivate() {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
if (guardClass && !roles?.length) {
|
|
11
|
+
return guardClass;
|
|
12
|
+
}
|
|
13
|
+
if (!guardClass && roles?.length) {
|
|
14
|
+
return class RolesOnlyGuard {
|
|
15
|
+
async canActivate(req) {
|
|
16
|
+
try {
|
|
17
|
+
const { getRequestUser } = await import("@cinnabun/auth");
|
|
18
|
+
const user = getRequestUser(req);
|
|
19
|
+
if (!user) {
|
|
20
|
+
throw new ForbiddenException("Authentication required");
|
|
21
|
+
}
|
|
22
|
+
if (!user.roles?.some((r) => roles.includes(r))) {
|
|
23
|
+
throw new ForbiddenException(`Requires one of roles: ${roles.join(", ")}`);
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (err instanceof ForbiddenException)
|
|
29
|
+
throw err;
|
|
30
|
+
throw new ForbiddenException("Roles check requires @cinnabun/auth. Install it and configure AuthGuard.");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const GuardClass = guardClass;
|
|
36
|
+
return class ComposedAdminGuard {
|
|
37
|
+
async canActivate(req) {
|
|
38
|
+
const guard = new GuardClass();
|
|
39
|
+
const passed = await guard.canActivate(req);
|
|
40
|
+
if (!passed)
|
|
41
|
+
return false;
|
|
42
|
+
try {
|
|
43
|
+
const { getRequestUser } = await import("@cinnabun/auth");
|
|
44
|
+
const user = getRequestUser(req);
|
|
45
|
+
if (!user) {
|
|
46
|
+
throw new ForbiddenException("Authentication required");
|
|
47
|
+
}
|
|
48
|
+
if (!user.roles?.some((r) => roles.includes(r))) {
|
|
49
|
+
throw new ForbiddenException(`Requires one of roles: ${roles.join(", ")}`);
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
if (err instanceof ForbiddenException)
|
|
55
|
+
throw err;
|
|
56
|
+
throw new ForbiddenException("Roles check requires @cinnabun/auth. Install it and configure AuthGuard.");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type { AdminModuleOptions } from "./interfaces/admin-options.js";
|
|
2
|
+
export type { AdminResource as AdminResourceConfig, Constructor, } from "./interfaces/admin-resource.js";
|
|
3
|
+
export type { AdminField as AdminFieldConfig, AdminFieldType } from "./interfaces/admin-field.js";
|
|
4
|
+
export { AdminService } from "./services/admin.service.js";
|
|
5
|
+
export type { AdminRepository, AdminMetadata, ListResult, } from "./services/admin.service.js";
|
|
6
|
+
export { AdminResource } from "./decorators/admin-resource.js";
|
|
7
|
+
export type { AdminResourceOptions } from "./decorators/admin-resource.js";
|
|
8
|
+
export { AdminField, getAdminFields } from "./decorators/admin-field.js";
|
|
9
|
+
export type { AdminFieldOptions } from "./decorators/admin-field.js";
|
|
10
|
+
export { AdminModule } from "./admin.module.js";
|
|
11
|
+
export { AdminPlugin } from "./admin.plugin.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { AdminService } from "./services/admin.service.js";
|
|
2
|
+
export { AdminResource } from "./decorators/admin-resource.js";
|
|
3
|
+
export { AdminField, getAdminFields } from "./decorators/admin-field.js";
|
|
4
|
+
export { AdminModule } from "./admin.module.js";
|
|
5
|
+
export { AdminPlugin } from "./admin.plugin.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type AdminFieldType = "text" | "number" | "boolean" | "date" | "select" | "textarea";
|
|
2
|
+
export interface AdminField {
|
|
3
|
+
name: string;
|
|
4
|
+
type: AdminFieldType;
|
|
5
|
+
label?: string;
|
|
6
|
+
required?: boolean;
|
|
7
|
+
options?: {
|
|
8
|
+
label: string;
|
|
9
|
+
value: unknown;
|
|
10
|
+
}[];
|
|
11
|
+
readonly?: boolean;
|
|
12
|
+
hidden?: boolean;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AdminResource } from "./admin-resource.js";
|
|
2
|
+
import type { Constructor } from "./admin-resource.js";
|
|
3
|
+
export interface AdminModuleOptions {
|
|
4
|
+
path?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
auth?: {
|
|
7
|
+
guard?: Constructor;
|
|
8
|
+
roles?: string[];
|
|
9
|
+
};
|
|
10
|
+
resources?: AdminResource[];
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AdminField } from "./admin-field.js";
|
|
2
|
+
export type Constructor<T = unknown> = new (...args: unknown[]) => T;
|
|
3
|
+
export interface AdminResource {
|
|
4
|
+
name: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
entity?: Constructor;
|
|
7
|
+
repository: Constructor;
|
|
8
|
+
fields: AdminField[];
|
|
9
|
+
list?: {
|
|
10
|
+
columns?: string[];
|
|
11
|
+
perPage?: number;
|
|
12
|
+
};
|
|
13
|
+
form?: {
|
|
14
|
+
fields?: string[];
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AdminResource } from "../interfaces/admin-resource.js";
|
|
2
|
+
declare class AdminMetadataStorage {
|
|
3
|
+
private resources;
|
|
4
|
+
addResource(resource: AdminResource): void;
|
|
5
|
+
getAllResources(): AdminResource[];
|
|
6
|
+
reset(): void;
|
|
7
|
+
}
|
|
8
|
+
export declare const adminMetadataStorage: AdminMetadataStorage;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class AdminMetadataStorage {
|
|
2
|
+
resources = [];
|
|
3
|
+
addResource(resource) {
|
|
4
|
+
const existing = this.resources.findIndex((r) => r.name === resource.name);
|
|
5
|
+
if (existing >= 0) {
|
|
6
|
+
this.resources[existing] = resource;
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
this.resources.push(resource);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
getAllResources() {
|
|
13
|
+
return [...this.resources];
|
|
14
|
+
}
|
|
15
|
+
reset() {
|
|
16
|
+
this.resources = [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export const adminMetadataStorage = new AdminMetadataStorage();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Container } from "@cinnabun/core";
|
|
2
|
+
import type { AdminResource } from "../interfaces/admin-resource.js";
|
|
3
|
+
export interface AdminRepository<T = unknown> {
|
|
4
|
+
findAll(options?: {
|
|
5
|
+
limit?: number;
|
|
6
|
+
offset?: number;
|
|
7
|
+
}): Promise<T[]>;
|
|
8
|
+
findById(id: string | number): Promise<T | null>;
|
|
9
|
+
create(data: Partial<T>): Promise<T>;
|
|
10
|
+
update(id: string | number, data: Partial<T>): Promise<T>;
|
|
11
|
+
delete(id: string | number): Promise<void>;
|
|
12
|
+
count?(where?: Partial<T>): Promise<number>;
|
|
13
|
+
}
|
|
14
|
+
export interface AdminMetadata {
|
|
15
|
+
title: string;
|
|
16
|
+
resources: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
label: string;
|
|
19
|
+
fields: AdminResource["fields"];
|
|
20
|
+
list?: AdminResource["list"];
|
|
21
|
+
form?: AdminResource["form"];
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export interface ListResult<T> {
|
|
25
|
+
data: T[];
|
|
26
|
+
total: number;
|
|
27
|
+
page: number;
|
|
28
|
+
perPage: number;
|
|
29
|
+
}
|
|
30
|
+
export declare class AdminService {
|
|
31
|
+
private container;
|
|
32
|
+
setContainer(container: Container): void;
|
|
33
|
+
private getResourceConfigs;
|
|
34
|
+
getResources(): AdminMetadata["resources"];
|
|
35
|
+
getResource(name: string): AdminResource | undefined;
|
|
36
|
+
private getRepository;
|
|
37
|
+
list<T>(resourceName: string, page?: number, perPage?: number): Promise<ListResult<T>>;
|
|
38
|
+
get<T>(resourceName: string, id: string | number): Promise<T | null>;
|
|
39
|
+
create<T>(resourceName: string, data: Record<string, unknown>): Promise<T>;
|
|
40
|
+
update<T>(resourceName: string, id: string | number, data: Record<string, unknown>): Promise<T>;
|
|
41
|
+
delete(resourceName: string, id: string | number): Promise<void>;
|
|
42
|
+
getMetadata(): AdminMetadata;
|
|
43
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AdminModule } from "../admin.module.js";
|
|
2
|
+
import { adminMetadataStorage } from "../metadata/admin-storage.js";
|
|
3
|
+
export class AdminService {
|
|
4
|
+
container = null;
|
|
5
|
+
setContainer(container) {
|
|
6
|
+
this.container = container;
|
|
7
|
+
}
|
|
8
|
+
getResourceConfigs() {
|
|
9
|
+
const options = AdminModule.getOptions();
|
|
10
|
+
const fromOptions = options.resources ?? [];
|
|
11
|
+
const fromDecorators = adminMetadataStorage.getAllResources();
|
|
12
|
+
const merged = new Map();
|
|
13
|
+
for (const r of [...fromDecorators, ...fromOptions]) {
|
|
14
|
+
merged.set(r.name, r);
|
|
15
|
+
}
|
|
16
|
+
return Array.from(merged.values());
|
|
17
|
+
}
|
|
18
|
+
getResources() {
|
|
19
|
+
return this.getResourceConfigs().map((r) => ({
|
|
20
|
+
name: r.name,
|
|
21
|
+
label: r.label ?? r.name.charAt(0).toUpperCase() + r.name.slice(1),
|
|
22
|
+
fields: r.fields,
|
|
23
|
+
list: r.list,
|
|
24
|
+
form: r.form,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
getResource(name) {
|
|
28
|
+
return this.getResourceConfigs().find((r) => r.name === name);
|
|
29
|
+
}
|
|
30
|
+
getRepository(resourceName) {
|
|
31
|
+
if (!this.container) {
|
|
32
|
+
throw new Error("AdminService: container not set. Add AdminPlugin to plugins and ensure it runs before admin routes are used.");
|
|
33
|
+
}
|
|
34
|
+
const resource = this.getResource(resourceName);
|
|
35
|
+
if (!resource) {
|
|
36
|
+
throw new Error(`Admin resource not found: ${resourceName}`);
|
|
37
|
+
}
|
|
38
|
+
const repo = this.container.resolve(resource.repository);
|
|
39
|
+
return repo;
|
|
40
|
+
}
|
|
41
|
+
async list(resourceName, page = 1, perPage = 25) {
|
|
42
|
+
const resource = this.getResource(resourceName);
|
|
43
|
+
if (!resource) {
|
|
44
|
+
throw new Error(`Admin resource not found: ${resourceName}`);
|
|
45
|
+
}
|
|
46
|
+
const effectivePerPage = resource.list?.perPage ?? perPage;
|
|
47
|
+
const offset = (page - 1) * effectivePerPage;
|
|
48
|
+
const repo = this.getRepository(resourceName);
|
|
49
|
+
const data = await repo.findAll({ limit: effectivePerPage, offset });
|
|
50
|
+
const total = typeof repo.count === "function" ? await repo.count() : data.length;
|
|
51
|
+
return {
|
|
52
|
+
data,
|
|
53
|
+
total,
|
|
54
|
+
page,
|
|
55
|
+
perPage: effectivePerPage,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async get(resourceName, id) {
|
|
59
|
+
const repo = this.getRepository(resourceName);
|
|
60
|
+
return repo.findById(id);
|
|
61
|
+
}
|
|
62
|
+
async create(resourceName, data) {
|
|
63
|
+
const repo = this.getRepository(resourceName);
|
|
64
|
+
return repo.create(data);
|
|
65
|
+
}
|
|
66
|
+
async update(resourceName, id, data) {
|
|
67
|
+
const repo = this.getRepository(resourceName);
|
|
68
|
+
return repo.update(id, data);
|
|
69
|
+
}
|
|
70
|
+
async delete(resourceName, id) {
|
|
71
|
+
const repo = this.getRepository(resourceName);
|
|
72
|
+
return repo.delete(id);
|
|
73
|
+
}
|
|
74
|
+
getMetadata() {
|
|
75
|
+
const options = AdminModule.getOptions();
|
|
76
|
+
return {
|
|
77
|
+
title: options.title ?? "Admin Dashboard",
|
|
78
|
+
resources: this.getResources(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildAdminUIHtml(title: string, basePath: string): string;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
export function buildAdminUIHtml(title, basePath) {
|
|
2
|
+
const apiBase = `${basePath}/api`;
|
|
3
|
+
const scriptContent = buildScriptContent(apiBase, basePath);
|
|
4
|
+
return `<!DOCTYPE html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="UTF-8">
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
9
|
+
<title>${escapeHtml(title)}</title>
|
|
10
|
+
<style>
|
|
11
|
+
* { box-sizing: border-box; }
|
|
12
|
+
body { margin: 0; font-family: system-ui, sans-serif; background: #f5f5f5; }
|
|
13
|
+
.layout { display: flex; min-height: 100vh; }
|
|
14
|
+
.sidebar { width: 220px; background: #1e293b; color: #e2e8f0; padding: 1rem 0; }
|
|
15
|
+
.sidebar h1 { margin: 0 1rem 1rem; font-size: 1.1rem; }
|
|
16
|
+
.sidebar a { display: block; padding: 0.5rem 1rem; color: #94a3b8; text-decoration: none; }
|
|
17
|
+
.sidebar a:hover, .sidebar a.active { background: #334155; color: #fff; }
|
|
18
|
+
.main { flex: 1; padding: 1.5rem; overflow: auto; }
|
|
19
|
+
.toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
|
|
20
|
+
.btn { padding: 0.5rem 1rem; background: #3b82f6; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9rem; }
|
|
21
|
+
.btn:hover { background: #2563eb; }
|
|
22
|
+
.btn-danger { background: #dc2626; }
|
|
23
|
+
.btn-danger:hover { background: #b91c1c; }
|
|
24
|
+
.btn-secondary { background: #64748b; }
|
|
25
|
+
.btn-secondary:hover { background: #475569; }
|
|
26
|
+
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
27
|
+
th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #e2e8f0; }
|
|
28
|
+
th { background: #f8fafc; font-weight: 600; }
|
|
29
|
+
tr:hover { background: #f8fafc; }
|
|
30
|
+
.form-group { margin-bottom: 1rem; }
|
|
31
|
+
.form-group label { display: block; margin-bottom: 0.25rem; font-weight: 500; }
|
|
32
|
+
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 0.5rem; border: 1px solid #cbd5e1; border-radius: 6px; }
|
|
33
|
+
.form-group input[type=checkbox] { width: auto; }
|
|
34
|
+
.pagination { display: flex; gap: 0.5rem; align-items: center; margin-top: 1rem; }
|
|
35
|
+
.pagination button { padding: 0.25rem 0.5rem; }
|
|
36
|
+
.empty { display: flex; min-height: 200px; align-items: center; justify-content: center; color: #64748b; }
|
|
37
|
+
.error { color: #dc2626; font-size: 0.9rem; margin-top: 0.5rem; }
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<div class="layout">
|
|
42
|
+
<nav class="sidebar">
|
|
43
|
+
<h1>${escapeHtml(title)}</h1>
|
|
44
|
+
<div id="sidebar-links"></div>
|
|
45
|
+
</nav>
|
|
46
|
+
<main class="main">
|
|
47
|
+
<div id="content"></div>
|
|
48
|
+
</main>
|
|
49
|
+
</div>
|
|
50
|
+
<script>${scriptContent}</script>
|
|
51
|
+
</body>
|
|
52
|
+
</html>`;
|
|
53
|
+
}
|
|
54
|
+
function buildScriptContent(apiBase, basePath) {
|
|
55
|
+
return `
|
|
56
|
+
(function() {
|
|
57
|
+
const API_BASE = ${JSON.stringify(apiBase)};
|
|
58
|
+
const BASE_PATH = ${JSON.stringify(basePath)};
|
|
59
|
+
let metadata = null;
|
|
60
|
+
|
|
61
|
+
function escapeHtml(s) {
|
|
62
|
+
if (s == null) return "";
|
|
63
|
+
const div = document.createElement("div");
|
|
64
|
+
div.textContent = s;
|
|
65
|
+
return div.innerHTML;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function fetchMetadata() {
|
|
69
|
+
const res = await fetch(API_BASE + "/metadata");
|
|
70
|
+
if (!res.ok) throw new Error("Failed to load metadata");
|
|
71
|
+
return res.json();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderSidebar() {
|
|
75
|
+
const container = document.getElementById("sidebar-links");
|
|
76
|
+
if (!metadata || !metadata.resources || !metadata.resources.length) {
|
|
77
|
+
container.innerHTML = "<p style='padding: 1rem; color: #94a3b8;'>No resources</p>";
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const path = window.location.pathname.replace(BASE_PATH, "").split("/").filter(Boolean);
|
|
81
|
+
const current = path[0] || "";
|
|
82
|
+
container.innerHTML = metadata.resources.map(function(r) {
|
|
83
|
+
return "<a href='#' data-resource='" + escapeHtml(r.name) + "' class='" + (current === r.name ? "active" : "") + "'>" + escapeHtml(r.label || r.name) + "</a>";
|
|
84
|
+
}).join("");
|
|
85
|
+
container.querySelectorAll("a").forEach(function(a) {
|
|
86
|
+
a.addEventListener("click", function(e) {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
navigate("list", a.dataset.resource);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function navigate(view, resource, id) {
|
|
94
|
+
const path = BASE_PATH + (resource ? "/" + resource + (view === "create" ? "/new" : id ? "/" + id : "") : "");
|
|
95
|
+
window.history.pushState({ view: view, resource: resource, id: id }, "", path);
|
|
96
|
+
render({ view: view, resource: resource, id: id });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
window.addEventListener("popstate", function(e) {
|
|
100
|
+
render(e.state || { view: "list", resource: null });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
async function render(state) {
|
|
104
|
+
const view = state && state.view || "list";
|
|
105
|
+
const resource = state && state.resource;
|
|
106
|
+
const id = state && state.id;
|
|
107
|
+
const content = document.getElementById("content");
|
|
108
|
+
|
|
109
|
+
document.querySelectorAll(".sidebar a").forEach(function(a) {
|
|
110
|
+
a.classList.toggle("active", a.dataset.resource === resource);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!resource) {
|
|
114
|
+
content.innerHTML = "<div class='empty'><p>Select a resource from the sidebar</p></div>";
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const res = metadata.resources.find(function(r) { return r.name === resource; });
|
|
119
|
+
if (!res) {
|
|
120
|
+
content.innerHTML = "<div class='empty'><p>Resource not found</p></div>";
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (view === "list") {
|
|
125
|
+
const page = parseInt(new URLSearchParams(window.location.search).get("page") || "1", 10);
|
|
126
|
+
const resp = await fetch(API_BASE + "/" + resource + "?page=" + page + "&perPage=25");
|
|
127
|
+
if (!resp.ok) { content.innerHTML = "<div class='error'>Failed to load data</div>"; return; }
|
|
128
|
+
const data = await resp.json();
|
|
129
|
+
const columns = data.data[0] ? Object.keys(data.data[0]) : (res.list && res.list.columns || (res.fields || []).map(function(f) { return f.name; }) || []);
|
|
130
|
+
const rowsHtml = data.data.length ? data.data.map(function(row) {
|
|
131
|
+
const rowId = row.id !== undefined ? row.id : Object.values(row)[0];
|
|
132
|
+
const cells = columns.map(function(c) { return "<td>" + escapeHtml(String(row[c] != null ? row[c] : "")) + "</td>"; }).join("");
|
|
133
|
+
return "<tr>" + cells + "<td><button class='btn btn-secondary' onclick='window.adminNavigate(\\"edit\\", \\"" + escapeHtml(resource) + "\\", \\"" + escapeHtml(String(rowId)) + "\\")'>Edit</button> <button class='btn btn-danger' onclick='window.adminDelete(\\"" + escapeHtml(resource) + "\\", \\"" + escapeHtml(String(rowId)) + "\\")'>Delete</button></td></tr>";
|
|
134
|
+
}).join("") : "<tr><td colspan='" + (columns.length + 1) + "'>No records</td></tr>";
|
|
135
|
+
const prevBtn = data.page > 1 ? "<button class='btn btn-secondary' onclick='window.adminGoPage(" + (data.page - 1) + ")'>Prev</button>" : "";
|
|
136
|
+
const nextBtn = (data.page * data.perPage < data.total) ? "<button class='btn btn-secondary' onclick='window.adminGoPage(" + (data.page + 1) + ")'>Next</button>" : "";
|
|
137
|
+
content.innerHTML = "<div class='toolbar'><h2>" + escapeHtml(res.label || resource) + "</h2><button class='btn' onclick='window.adminNavigate(\\"create\\", \\"" + escapeHtml(resource) + "\\")'>Create</button></div>" +
|
|
138
|
+
"<table><thead><tr>" + columns.map(function(c) { return "<th>" + escapeHtml(c) + "</th>"; }).join("") + "<th>Actions</th></tr></thead><tbody>" + rowsHtml + "</tbody></table>" +
|
|
139
|
+
"<div class='pagination'>" + prevBtn + "<span>Page " + data.page + " of " + (Math.ceil(data.total / data.perPage) || 1) + "</span>" + nextBtn + "</div>";
|
|
140
|
+
} else if (view === "create" || view === "edit") {
|
|
141
|
+
const formFields = res.fields && res.fields.filter(function(f) {
|
|
142
|
+
const fields = (res.form && res.form.fields) || (res.fields || []).map(function(x) { return x.name; }) || [];
|
|
143
|
+
return fields.length === 0 || fields.indexOf(f.name) >= 0;
|
|
144
|
+
}) || [];
|
|
145
|
+
let values = {};
|
|
146
|
+
if (view === "edit" && id) {
|
|
147
|
+
const r = await fetch(API_BASE + "/" + resource + "/" + id);
|
|
148
|
+
if (r.ok) values = await r.json();
|
|
149
|
+
}
|
|
150
|
+
const formHtml = formFields.map(function(f) {
|
|
151
|
+
const val = values[f.name];
|
|
152
|
+
const v = val !== undefined && val !== null ? String(val) : "";
|
|
153
|
+
if (f.type === "boolean") return "<div class='form-group'><label><input type='checkbox' name='" + escapeHtml(f.name) + "' " + (v === "true" ? "checked" : "") + "> " + escapeHtml(f.label || f.name) + "</label></div>";
|
|
154
|
+
if (f.type === "select") return "<div class='form-group'><label>" + escapeHtml(f.label || f.name) + "</label><select name='" + escapeHtml(f.name) + "' " + (f.required ? "required" : "") + ">" + (f.options || []).map(function(o) { return "<option value='" + escapeHtml(String(o.value)) + "' " + (v === String(o.value) ? "selected" : "") + ">" + escapeHtml(o.label) + "</option>"; }).join("") + "</select></div>";
|
|
155
|
+
if (f.type === "textarea") return "<div class='form-group'><label>" + escapeHtml(f.label || f.name) + "</label><textarea name='" + escapeHtml(f.name) + "' " + (f.readonly ? "readonly" : "") + " " + (f.required ? "required" : "") + ">" + escapeHtml(v) + "</textarea></div>";
|
|
156
|
+
return "<div class='form-group'><label>" + escapeHtml(f.label || f.name) + "</label><input type='" + (f.type === "date" ? "date" : f.type === "number" ? "number" : "text") + "' name='" + escapeHtml(f.name) + "' value='" + escapeHtml(v) + "' " + (f.readonly ? "readonly" : "") + " " + (f.required ? "required" : "") + "></div>";
|
|
157
|
+
}).join("");
|
|
158
|
+
const formId = view === "edit" ? JSON.stringify(id) : "null";
|
|
159
|
+
content.innerHTML = "<div class='toolbar'><h2>" + (view === "create" ? "Create" : "Edit") + " " + escapeHtml(res.label || resource) + "</h2><button class='btn btn-secondary' onclick='window.adminNavigate(\\"list\\", \\"" + escapeHtml(resource) + "\\")'>Back</button></div>" +
|
|
160
|
+
"<form id='admin-form' onsubmit='return window.adminSubmitForm(event, " + JSON.stringify(resource) + ", " + formId + ")'>" + formHtml + "<button type='submit' class='btn'>Save</button></form>";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
window.adminNavigate = function(view, resource, id) {
|
|
165
|
+
navigate(view, resource, id);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
window.adminDelete = async function(resource, id) {
|
|
169
|
+
if (!confirm("Delete this record?")) return;
|
|
170
|
+
const res = await fetch(API_BASE + "/" + resource + "/" + id, { method: "DELETE" });
|
|
171
|
+
if (res.ok) navigate("list", resource);
|
|
172
|
+
else alert("Failed to delete");
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
window.adminSubmitForm = async function(e, resource, id) {
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
const form = e.target;
|
|
178
|
+
const formData = new FormData(form);
|
|
179
|
+
const obj = {};
|
|
180
|
+
formData.forEach(function(v, k) {
|
|
181
|
+
const field = metadata.resources.find(function(r) { return r.name === resource; }) && metadata.resources.find(function(r) { return r.name === resource; }).fields && metadata.resources.find(function(r) { return r.name === resource; }).fields.find(function(f) { return f.name === k; });
|
|
182
|
+
if (field && field.type === "number") obj[k] = parseInt(v, 10) || 0;
|
|
183
|
+
else if (field && field.type === "boolean") obj[k] = formData.get(k) === "on";
|
|
184
|
+
else obj[k] = v;
|
|
185
|
+
});
|
|
186
|
+
const url = id ? API_BASE + "/" + resource + "/" + id : API_BASE + "/" + resource;
|
|
187
|
+
const method = id ? "PUT" : "POST";
|
|
188
|
+
const res = await fetch(url, { method: method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(obj) });
|
|
189
|
+
if (!res.ok) { alert("Failed to save"); return false; }
|
|
190
|
+
navigate("list", resource);
|
|
191
|
+
return false;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
window.adminGoPage = function(page) {
|
|
195
|
+
const url = new URL(window.location.href);
|
|
196
|
+
url.searchParams.set("page", page);
|
|
197
|
+
window.location.href = url.pathname + url.search;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
(async function() {
|
|
201
|
+
try {
|
|
202
|
+
metadata = await fetchMetadata();
|
|
203
|
+
renderSidebar();
|
|
204
|
+
const path = window.location.pathname.replace(BASE_PATH, "").split("/").filter(Boolean);
|
|
205
|
+
const resource = path[0];
|
|
206
|
+
const second = path[1];
|
|
207
|
+
if (resource && second === "new") {
|
|
208
|
+
render({ view: "create", resource: resource });
|
|
209
|
+
} else if (resource && second && second !== "new") {
|
|
210
|
+
render({ view: "edit", resource: resource, id: second });
|
|
211
|
+
} else if (resource) {
|
|
212
|
+
render({ view: "list", resource: resource });
|
|
213
|
+
} else {
|
|
214
|
+
render({ view: "list", resource: null });
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
document.getElementById("content").innerHTML = "<div class='error'>Failed to load: " + escapeHtml(err.message) + "</div>";
|
|
218
|
+
}
|
|
219
|
+
})();
|
|
220
|
+
})();
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
function escapeHtml(s) {
|
|
224
|
+
return s
|
|
225
|
+
.replace(/&/g, "&")
|
|
226
|
+
.replace(/</g, "<")
|
|
227
|
+
.replace(/>/g, ">")
|
|
228
|
+
.replace(/"/g, """)
|
|
229
|
+
.replace(/'/g, "'");
|
|
230
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cinnabun/admin",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Auto-generated admin UI for CRUD operations",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": ["dist"],
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "bun test",
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"prepublishOnly": "bun run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["cinnabun", "admin", "crud"],
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@cinnabun/core": "^0.0.3",
|
|
20
|
+
"@cinnabun/db": ">=0.0.1",
|
|
21
|
+
"@cinnabun/auth": ">=0.0.1"
|
|
22
|
+
},
|
|
23
|
+
"peerDependenciesMeta": {
|
|
24
|
+
"@cinnabun/db": { "optional": true },
|
|
25
|
+
"@cinnabun/auth": { "optional": true }
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@cinnabun/core": "workspace:*",
|
|
29
|
+
"@cinnabun/testing": "workspace:*",
|
|
30
|
+
"@types/bun": "latest",
|
|
31
|
+
"reflect-metadata": "^0.2.2",
|
|
32
|
+
"typescript": "^5.9.3"
|
|
33
|
+
}
|
|
34
|
+
}
|