@maykonpaulo/maestro-server 0.1.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # @maykonpaulo/maestro-server
2
+
3
+ The **turnkey HTTP server** for [`@maykonpaulo/maestro-core`](https://www.npmjs.com/package/@maykonpaulo/maestro-core), per [ADR 0008 — Camada Turnkey](../../docs/adr/0008-turnkey-server-and-admin-ui.md).
4
+
5
+ The core is deliberately server-free and framework-agnostic: it exposes `createMaestroHttpHandlers` (handlers you mount on *your* Express/Fastify/Lambda), never an actual server. This sibling package is the batteries-included layer that boots a real HTTP server, and — combined with a live datasource provider — turns **any database into a complete, governed admin over every collection, without writing a line of per-entity code**.
6
+
7
+ It adds only what a server needs and the core must not carry: JSON body parsing, CORS, route wiring and a `listen()`/`close()` lifecycle. All domain behaviour (RBAC, audit, governance, CRUD) still comes from the engine.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @maykonpaulo/maestro-server @maykonpaulo/maestro-core
13
+ ```
14
+
15
+ `@maykonpaulo/maestro-core` is a peer dependency. `express` is a direct dependency of this package.
16
+
17
+ ## Point at a database, manipulate everything (introspection mode)
18
+
19
+ The fastest path: hand it a live provider (e.g. [`@maykonpaulo/maestro-provider-mongodb`](../provider-mongodb)) and get a full admin over every discovered collection — no declarations required.
20
+
21
+ ```ts
22
+ import { createMongoProvider } from '@maykonpaulo/maestro-provider-mongodb';
23
+ import { createMaestroServer, createIntrospectedEngine } from '@maykonpaulo/maestro-server';
24
+
25
+ const provider = createMongoProvider({ uri: process.env.MONGO_URL!, database: 'app' });
26
+
27
+ // Introspects the live DB and builds an engine over every collection, with write access enabled.
28
+ const engine = await createIntrospectedEngine({ provider, access: 'full' });
29
+
30
+ const server = createMaestroServer({ engine });
31
+ await server.listen(3000);
32
+ // → REST admin for every collection at http://localhost:3000/entities/:collection
33
+ ```
34
+
35
+ `access: 'readonly'` enables only `list`/`detail`/`export` — a safe way to browse a production database without any risk of mutating it. `access: 'full'` (default) enables the complete CRUD surface.
36
+
37
+ > Entities discovered by introspection are read-only under the core's `createMaestroFromIntrospection`; `createIntrospectedEngine` is the piece that enables write capabilities according to `access`, so "manipulate everything" works out of the box.
38
+
39
+ ## Curated mode (declarations)
40
+
41
+ When you want nice labels, fine-grained RBAC and governance on chosen entities, build the engine yourself with `createMaestro` and just serve it:
42
+
43
+ ```ts
44
+ import { createMaestro } from '@maykonpaulo/maestro-core';
45
+ import { createMaestroServer } from '@maykonpaulo/maestro-server';
46
+
47
+ const engine = createMaestro({ datasources: { main: provider }, declarations, policies, audit });
48
+ const server = createMaestroServer({ engine, actorResolver });
49
+ await server.listen(3000);
50
+ ```
51
+
52
+ Both modes can be combined: pass an `overrides` (curated) config to `createIntrospectedEngine` and the core `HybridLoader` merges it over the introspected schema.
53
+
54
+ ## Authentication is yours (the `actorResolver` hook)
55
+
56
+ Authentication stays the consumer's responsibility. `createMaestroServer` takes an `actorResolver` that derives the acting `Actor` from each request (a JWT, a cookie, a session):
57
+
58
+ ```ts
59
+ const server = createMaestroServer({
60
+ engine,
61
+ actorResolver: async (req) => {
62
+ const user = await verifyJwt(req.headers['authorization']);
63
+ return { actor: { id: user.id, type: 'user', roles: user.roles } };
64
+ },
65
+ });
66
+ ```
67
+
68
+ When omitted, a **development-only** `devActorResolver` grants every request a single admin actor. Never use it in production.
69
+
70
+ ## API
71
+
72
+ ### `createMaestroServer(options): MaestroServer`
73
+
74
+ | Option | Description |
75
+ |---|---|
76
+ | `engine` | The `MaestroEngine` to serve (from `createMaestro` or `createIntrospectedEngine`). |
77
+ | `actorResolver?` | Resolves the `Actor` per request. Defaults to `devActorResolver` (dev only). |
78
+ | `cors?` | `true`/omitted → permissive defaults; an object → configured (`origin`, `credentials`, `allowedHeaders`); `false` → no CORS middleware. |
79
+ | `basePath?` | Path prefix for every route (e.g. `/api/admin`). |
80
+ | `app?` | Bring your own Express app (to add auth middleware/other routes). When given, JSON/CORS are not auto-applied. |
81
+
82
+ Returns `{ app, handlers, listen(port, host?) }`. `listen` resolves with the Node `http.Server` once bound.
83
+
84
+ ### `createIntrospectedEngine(options): Promise<MaestroEngine>`
85
+
86
+ | Option | Description |
87
+ |---|---|
88
+ | `provider` | A provider implementing both `IntrospectionProvider` and `DatasourceProvider`. |
89
+ | `access?` | `'full'` (default) enables all capabilities; `'readonly'` enables list/detail/export only. |
90
+ | `datasourceId?` | Datasource id the entities bind to. Defaults to `main`. |
91
+ | `overrides?` | Optional curated overlay merged via the core `HybridLoader`. |
92
+ | `policies?` | RBAC policy. Defaults to an `admin` role with `['*']`. |
93
+ | `operations?`, `audit?`, `logger?` | Forwarded to the engine. |
94
+
95
+ ## Routes
96
+
97
+ ```
98
+ GET /health
99
+ GET /metadata
100
+ GET /metadata/:entity
101
+ GET /entities/:entity (list — filter/sort/paginate/search via query string)
102
+ POST /entities/:entity (create)
103
+ GET /entities/:entity/:id (detail)
104
+ PATCH /entities/:entity/:id (update)
105
+ DELETE /entities/:entity/:id (hard delete)
106
+ POST /entities/:entity/:id/clone
107
+ POST /entities/:entity/:id/soft-delete
108
+ POST /entities/:entity/:id/restore
109
+ GET /entities/:entity/export (?format=csv|json)
110
+ POST /operations/:operationId
111
+ ```
112
+
113
+ ## What this package is not
114
+
115
+ It bundles **no database driver** — it depends on a `DatasourceProvider` you instantiate (the driver comes with the provider package). It does **not** handle authentication or identity — that is the `actorResolver`'s job. Those boundaries are intentional and preserve the core's purity (ADR 0008).
@@ -0,0 +1,101 @@
1
+ import { Server } from 'node:http';
2
+ import { Express } from 'express';
3
+ import { MaestroEngine, MaestroActorResolver, MaestroHttpHandlers, IntrospectionProvider, DatasourceProvider, LoadedConfig, RbacPolicy, OperationDef, AuditRepository, Logger } from '@maykonpaulo/maestro-core';
4
+
5
+ interface CorsOptions {
6
+ /** Allowed origin. Defaults to `*`. Set to a concrete origin when using credentials. */
7
+ origin?: string;
8
+ /** Emit `Access-Control-Allow-Credentials: true`. Defaults to false. */
9
+ credentials?: boolean;
10
+ /** Extra request headers to allow, appended to the defaults. */
11
+ allowedHeaders?: string[];
12
+ }
13
+ /**
14
+ * A dependency-free CORS middleware. Kept tiny on purpose — the turnkey server should not pull an extra
15
+ * package for a handful of headers, and a consumer with stricter needs can disable this and mount their own.
16
+ */
17
+ declare function applyCors(app: Express, options?: CorsOptions): void;
18
+
19
+ interface CreateMaestroServerOptions {
20
+ /** The engine to serve. Build it from declarations (`createMaestro`) or from a live database (`createIntrospectedEngine`). */
21
+ engine: MaestroEngine;
22
+ /**
23
+ * Resolves the acting `Actor` for each request (authentication is the consumer's responsibility).
24
+ * Defaults to {@link devActorResolver} — a single admin actor, suitable only for local development.
25
+ */
26
+ actorResolver?: MaestroActorResolver;
27
+ /** CORS handling. `true`/omitted → permissive defaults; an object → configured; `false` → no CORS middleware. */
28
+ cors?: boolean | CorsOptions;
29
+ /** Path prefix for every route (e.g. `/api/admin`). Defaults to none. */
30
+ basePath?: string;
31
+ /**
32
+ * Bring your own Express app to mount the routes on (to add auth middleware, logging, other routes, etc.).
33
+ * When omitted, a fresh app is created with a JSON body parser and CORS.
34
+ */
35
+ app?: Express;
36
+ }
37
+ interface MaestroServer {
38
+ /** The Express app with all Maestro routes mounted. Mount extra routes/middleware before calling `listen`. */
39
+ app: Express;
40
+ /** The underlying framework-agnostic handlers, in case you want to mount a subset yourself. */
41
+ handlers: MaestroHttpHandlers;
42
+ /** Starts listening and resolves with the Node `http.Server` once bound. */
43
+ listen(port: number, host?: string): Promise<Server>;
44
+ }
45
+ /**
46
+ * Mounts the Maestro HTTP handlers on a real Express server — the turnkey layer the core deliberately
47
+ * omits (ADR 0008). It adds only what a server needs and the core must not carry: JSON body parsing,
48
+ * CORS, route wiring and a `listen()` lifecycle. All domain behaviour still comes from the engine.
49
+ */
50
+ declare function createMaestroServer(options: CreateMaestroServerOptions): MaestroServer;
51
+
52
+ /** Access level applied to entities discovered by introspection. */
53
+ type IntrospectedAccess = 'full' | 'readonly';
54
+ interface CreateIntrospectedEngineOptions {
55
+ /**
56
+ * A live provider that can both describe the database (`introspect`) and operate it (`list/create/...`).
57
+ * The MongoDB, SQL, and other official providers implement both interfaces on a single instance.
58
+ */
59
+ provider: IntrospectionProvider & DatasourceProvider;
60
+ /** Datasource id the discovered entities are bound to. Defaults to `main`. */
61
+ datasourceId?: string;
62
+ /**
63
+ * `full` (default) enables every capability on discovered entities, turning the engine into a complete
64
+ * database manipulator. `readonly` enables only list/detail/export — a safe way to browse a database
65
+ * without any risk of mutating it.
66
+ */
67
+ access?: IntrospectedAccess;
68
+ /**
69
+ * Optional curated overlay (labels, extra fields, relations, RBAC) merged over the introspected schema
70
+ * via the core `HybridLoader`. Entities you curate here keep their own explicit `capabilities`.
71
+ */
72
+ overrides?: LoadedConfig;
73
+ /**
74
+ * RBAC policy for the engine. When omitted, a default `admin` role with `['*']` is used — appropriate
75
+ * for a single platform operator and meant to be replaced with a real policy in multi-role deployments.
76
+ */
77
+ policies?: RbacPolicy;
78
+ operations?: OperationDef[];
79
+ audit?: AuditRepository;
80
+ logger?: Logger;
81
+ }
82
+ /**
83
+ * Builds a `MaestroEngine` directly from a live database, with no per-entity declaration required
84
+ * (ADR 0008, DA-17). It introspects the provider, turns every discovered collection/table into a
85
+ * manipulable entity, and — unlike `createMaestroFromIntrospection`, whose entities are read-only by
86
+ * default — enables write capabilities according to `access`, so "point at the database and manipulate
87
+ * everything" works out of the box.
88
+ *
89
+ * Uses only public core exports (`introspect` → `mergeIntrospectionWithOverrides` → `createMaestro`).
90
+ */
91
+ declare function createIntrospectedEngine(options: CreateIntrospectedEngineOptions): Promise<MaestroEngine>;
92
+
93
+ /**
94
+ * The default resolver used when `createMaestroServer` is called without one. It grants every request a
95
+ * single admin actor — convenient for local development and demos, and **never** appropriate for
96
+ * production. In production the consumer supplies its own resolver (deriving the actor from a JWT, cookie
97
+ * or session), keeping authentication the consumer's responsibility (ADR 0008, DA-18).
98
+ */
99
+ declare const devActorResolver: MaestroActorResolver;
100
+
101
+ export { type CorsOptions, type CreateIntrospectedEngineOptions, type CreateMaestroServerOptions, type IntrospectedAccess, type MaestroServer, applyCors, createIntrospectedEngine, createMaestroServer, devActorResolver };
package/dist/index.js ADDED
@@ -0,0 +1,188 @@
1
+ // src/createMaestroServer.ts
2
+ import { createServer } from "http";
3
+ import express from "express";
4
+ import { createMaestroHttpHandlers } from "@maykonpaulo/maestro-core";
5
+
6
+ // src/expressAdapter.ts
7
+ function toMaestroRequest(req) {
8
+ return {
9
+ params: req.params,
10
+ query: req.query,
11
+ body: req.body,
12
+ headers: req.headers,
13
+ method: req.method,
14
+ path: req.path
15
+ };
16
+ }
17
+ function adapt(handler) {
18
+ return async (req, res) => {
19
+ const response = await handler(toMaestroRequest(req));
20
+ if (response.headers) {
21
+ for (const [key, value] of Object.entries(response.headers)) {
22
+ res.setHeader(key, value);
23
+ }
24
+ }
25
+ res.status(response.status);
26
+ if (response.body === null || response.body === void 0) {
27
+ res.end();
28
+ } else if (typeof response.body === "string") {
29
+ res.send(response.body);
30
+ } else {
31
+ res.json(response.body);
32
+ }
33
+ };
34
+ }
35
+
36
+ // src/cors.ts
37
+ var DEFAULT_HEADERS = ["Content-Type", "Authorization", "X-Role"];
38
+ var ALLOWED_METHODS = "GET,POST,PATCH,DELETE,OPTIONS";
39
+ function applyCors(app, options = {}) {
40
+ const headers = [...DEFAULT_HEADERS, ...options.allowedHeaders ?? []].join(", ");
41
+ app.use((req, res, next) => {
42
+ res.setHeader("Access-Control-Allow-Origin", options.origin ?? "*");
43
+ res.setHeader("Access-Control-Allow-Methods", ALLOWED_METHODS);
44
+ res.setHeader("Access-Control-Allow-Headers", headers);
45
+ if (options.credentials) res.setHeader("Access-Control-Allow-Credentials", "true");
46
+ if (req.method === "OPTIONS") {
47
+ res.status(204).end();
48
+ return;
49
+ }
50
+ next();
51
+ });
52
+ }
53
+
54
+ // src/devActorResolver.ts
55
+ var devActorResolver = async () => ({
56
+ actor: {
57
+ id: "dev-admin",
58
+ type: "user",
59
+ name: "Development Admin",
60
+ roles: ["admin"]
61
+ }
62
+ });
63
+
64
+ // src/createMaestroServer.ts
65
+ var ROUTES = {
66
+ health: "/health",
67
+ metadata: "/metadata",
68
+ entityMetadata: "/metadata/:entity",
69
+ operation: "/operations/:operationId",
70
+ export: "/entities/:entity/export",
71
+ list: "/entities/:entity",
72
+ create: "/entities/:entity",
73
+ findById: "/entities/:entity/:id",
74
+ update: "/entities/:entity/:id",
75
+ clone: "/entities/:entity/:id/clone",
76
+ softDelete: "/entities/:entity/:id/soft-delete",
77
+ restore: "/entities/:entity/:id/restore",
78
+ hardDelete: "/entities/:entity/:id"
79
+ };
80
+ function createMaestroServer(options) {
81
+ const actorResolver = options.actorResolver ?? devActorResolver;
82
+ const handlers = createMaestroHttpHandlers(options.engine, { actorResolver });
83
+ const app = options.app ?? express();
84
+ if (!options.app) {
85
+ if (options.cors !== false) {
86
+ applyCors(app, typeof options.cors === "object" ? options.cors : {});
87
+ }
88
+ app.use(express.json());
89
+ }
90
+ const base = options.basePath ?? "";
91
+ const at = (path) => `${base}${path}`;
92
+ app.get(at(ROUTES.health), adapt(handlers.health));
93
+ app.get(at(ROUTES.metadata), adapt(handlers.getMetadata));
94
+ app.get(at(ROUTES.entityMetadata), adapt(handlers.getEntityMetadata));
95
+ app.post(at(ROUTES.operation), adapt(handlers.executeOperation));
96
+ app.get(at(ROUTES.export), adapt(handlers.export));
97
+ app.get(at(ROUTES.list), adapt(handlers.list));
98
+ app.post(at(ROUTES.create), adapt(handlers.create));
99
+ app.get(at(ROUTES.findById), adapt(handlers.findById));
100
+ app.patch(at(ROUTES.update), adapt(handlers.update));
101
+ app.post(at(ROUTES.clone), adapt(handlers.clone));
102
+ app.post(at(ROUTES.softDelete), adapt(handlers.softDelete));
103
+ app.post(at(ROUTES.restore), adapt(handlers.restore));
104
+ app.delete(at(ROUTES.hardDelete), adapt(handlers.hardDelete));
105
+ return {
106
+ app,
107
+ handlers,
108
+ listen(port, host) {
109
+ const server = createServer(app);
110
+ return new Promise((resolve, reject) => {
111
+ server.once("error", reject);
112
+ const onReady = () => {
113
+ server.off("error", reject);
114
+ resolve(server);
115
+ };
116
+ if (host) server.listen(port, host, onReady);
117
+ else server.listen(port, onReady);
118
+ });
119
+ }
120
+ };
121
+ }
122
+
123
+ // src/createIntrospectedEngine.ts
124
+ import {
125
+ createMaestro,
126
+ mergeIntrospectionWithOverrides
127
+ } from "@maykonpaulo/maestro-core";
128
+ var FULL_CAPABILITIES = {
129
+ list: true,
130
+ detail: true,
131
+ create: true,
132
+ update: true,
133
+ clone: true,
134
+ delete: true,
135
+ softDelete: true,
136
+ export: true,
137
+ bulkActions: true
138
+ };
139
+ var READONLY_CAPABILITIES = {
140
+ list: true,
141
+ detail: true,
142
+ create: false,
143
+ update: false,
144
+ clone: false,
145
+ delete: false,
146
+ softDelete: false,
147
+ export: true,
148
+ bulkActions: false
149
+ };
150
+ var DEFAULT_ADMIN_POLICY = {
151
+ roles: {
152
+ admin: { id: "admin", name: "Administrator", permissions: ["*"] }
153
+ }
154
+ };
155
+ function capabilitiesFor(entity, access) {
156
+ if (entity.capabilities) return entity.capabilities;
157
+ const base = access === "full" ? FULL_CAPABILITIES : READONLY_CAPABILITIES;
158
+ return { ...base, softDelete: access === "full" ? Boolean(entity.softDelete) : false };
159
+ }
160
+ async function createIntrospectedEngine(options) {
161
+ const datasourceId = options.datasourceId ?? "main";
162
+ const access = options.access ?? "full";
163
+ const introspection = await options.provider.introspect();
164
+ const overrides = options.overrides ?? { entities: [], relations: [] };
165
+ const config = mergeIntrospectionWithOverrides({
166
+ introspection,
167
+ overrides,
168
+ datasources: { [datasourceId]: options.provider },
169
+ defaultDatasource: datasourceId,
170
+ strategy: "extend",
171
+ operations: options.operations,
172
+ audit: options.audit,
173
+ logger: options.logger
174
+ });
175
+ config.entities = (config.entities ?? []).map((entity) => ({
176
+ ...entity,
177
+ capabilities: capabilitiesFor(entity, access)
178
+ }));
179
+ config.policies = options.policies ?? config.policies ?? DEFAULT_ADMIN_POLICY;
180
+ return createMaestro(config);
181
+ }
182
+ export {
183
+ applyCors,
184
+ createIntrospectedEngine,
185
+ createMaestroServer,
186
+ devActorResolver
187
+ };
188
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/createMaestroServer.ts","../src/expressAdapter.ts","../src/cors.ts","../src/devActorResolver.ts","../src/createIntrospectedEngine.ts"],"sourcesContent":["import { createServer, type Server } from 'node:http';\nimport express, { type Express } from 'express';\nimport { createMaestroHttpHandlers } from '@maykonpaulo/maestro-core';\nimport type {\n MaestroEngine,\n MaestroActorResolver,\n MaestroHttpHandlers,\n} from '@maykonpaulo/maestro-core';\nimport { adapt } from './expressAdapter.js';\nimport { applyCors, type CorsOptions } from './cors.js';\nimport { devActorResolver } from './devActorResolver.js';\n\nexport interface CreateMaestroServerOptions {\n /** The engine to serve. Build it from declarations (`createMaestro`) or from a live database (`createIntrospectedEngine`). */\n engine: MaestroEngine;\n /**\n * Resolves the acting `Actor` for each request (authentication is the consumer's responsibility).\n * Defaults to {@link devActorResolver} — a single admin actor, suitable only for local development.\n */\n actorResolver?: MaestroActorResolver;\n /** CORS handling. `true`/omitted → permissive defaults; an object → configured; `false` → no CORS middleware. */\n cors?: boolean | CorsOptions;\n /** Path prefix for every route (e.g. `/api/admin`). Defaults to none. */\n basePath?: string;\n /**\n * Bring your own Express app to mount the routes on (to add auth middleware, logging, other routes, etc.).\n * When omitted, a fresh app is created with a JSON body parser and CORS.\n */\n app?: Express;\n}\n\nexport interface MaestroServer {\n /** The Express app with all Maestro routes mounted. Mount extra routes/middleware before calling `listen`. */\n app: Express;\n /** The underlying framework-agnostic handlers, in case you want to mount a subset yourself. */\n handlers: MaestroHttpHandlers;\n /** Starts listening and resolves with the Node `http.Server` once bound. */\n listen(port: number, host?: string): Promise<Server>;\n}\n\nconst ROUTES = {\n health: '/health',\n metadata: '/metadata',\n entityMetadata: '/metadata/:entity',\n operation: '/operations/:operationId',\n export: '/entities/:entity/export',\n list: '/entities/:entity',\n create: '/entities/:entity',\n findById: '/entities/:entity/:id',\n update: '/entities/:entity/:id',\n clone: '/entities/:entity/:id/clone',\n softDelete: '/entities/:entity/:id/soft-delete',\n restore: '/entities/:entity/:id/restore',\n hardDelete: '/entities/:entity/:id',\n} as const;\n\n/**\n * Mounts the Maestro HTTP handlers on a real Express server — the turnkey layer the core deliberately\n * omits (ADR 0008). It adds only what a server needs and the core must not carry: JSON body parsing,\n * CORS, route wiring and a `listen()` lifecycle. All domain behaviour still comes from the engine.\n */\nexport function createMaestroServer(options: CreateMaestroServerOptions): MaestroServer {\n const actorResolver = options.actorResolver ?? devActorResolver;\n const handlers = createMaestroHttpHandlers(options.engine, { actorResolver });\n\n const app = options.app ?? express();\n\n if (!options.app) {\n if (options.cors !== false) {\n applyCors(app, typeof options.cors === 'object' ? options.cors : {});\n }\n app.use(express.json());\n }\n\n const base = options.basePath ?? '';\n const at = (path: string): string => `${base}${path}`;\n\n app.get(at(ROUTES.health), adapt(handlers.health));\n app.get(at(ROUTES.metadata), adapt(handlers.getMetadata));\n app.get(at(ROUTES.entityMetadata), adapt(handlers.getEntityMetadata));\n\n app.post(at(ROUTES.operation), adapt(handlers.executeOperation));\n\n // `/entities/:entity/export` must be registered before `/entities/:entity/:id` so \"export\" isn't\n // captured as an id.\n app.get(at(ROUTES.export), adapt(handlers.export));\n app.get(at(ROUTES.list), adapt(handlers.list));\n app.post(at(ROUTES.create), adapt(handlers.create));\n app.get(at(ROUTES.findById), adapt(handlers.findById));\n app.patch(at(ROUTES.update), adapt(handlers.update));\n app.post(at(ROUTES.clone), adapt(handlers.clone));\n app.post(at(ROUTES.softDelete), adapt(handlers.softDelete));\n app.post(at(ROUTES.restore), adapt(handlers.restore));\n app.delete(at(ROUTES.hardDelete), adapt(handlers.hardDelete));\n\n return {\n app,\n handlers,\n listen(port: number, host?: string): Promise<Server> {\n const server = createServer(app);\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n const onReady = (): void => {\n server.off('error', reject);\n resolve(server);\n };\n if (host) server.listen(port, host, onReady);\n else server.listen(port, onReady);\n });\n },\n };\n}\n","import type { Request, Response } from 'express';\nimport type { MaestroHttpHandler, MaestroHttpRequest } from '@maykonpaulo/maestro-core';\n\nfunction toMaestroRequest(req: Request): MaestroHttpRequest {\n return {\n params: req.params as Record<string, string>,\n query: req.query as Record<string, string | string[]>,\n body: req.body as unknown,\n headers: req.headers as Record<string, string | string[] | undefined>,\n method: req.method,\n path: req.path,\n };\n}\n\n/**\n * Bridges a framework-agnostic `MaestroHttpHandler` (from the core) to an Express request handler.\n * The core owns the contract; this adapter is the only Express-aware surface in the package.\n */\nexport function adapt(handler: MaestroHttpHandler) {\n return async (req: Request, res: Response): Promise<void> => {\n const response = await handler(toMaestroRequest(req));\n\n if (response.headers) {\n for (const [key, value] of Object.entries(response.headers)) {\n res.setHeader(key, value);\n }\n }\n\n res.status(response.status);\n\n if (response.body === null || response.body === undefined) {\n res.end();\n } else if (typeof response.body === 'string') {\n res.send(response.body);\n } else {\n res.json(response.body);\n }\n };\n}\n","import type { Express, Request, Response, NextFunction } from 'express';\n\nexport interface CorsOptions {\n /** Allowed origin. Defaults to `*`. Set to a concrete origin when using credentials. */\n origin?: string;\n /** Emit `Access-Control-Allow-Credentials: true`. Defaults to false. */\n credentials?: boolean;\n /** Extra request headers to allow, appended to the defaults. */\n allowedHeaders?: string[];\n}\n\nconst DEFAULT_HEADERS = ['Content-Type', 'Authorization', 'X-Role'];\nconst ALLOWED_METHODS = 'GET,POST,PATCH,DELETE,OPTIONS';\n\n/**\n * A dependency-free CORS middleware. Kept tiny on purpose — the turnkey server should not pull an extra\n * package for a handful of headers, and a consumer with stricter needs can disable this and mount their own.\n */\nexport function applyCors(app: Express, options: CorsOptions = {}): void {\n const headers = [...DEFAULT_HEADERS, ...(options.allowedHeaders ?? [])].join(', ');\n app.use((req: Request, res: Response, next: NextFunction): void => {\n res.setHeader('Access-Control-Allow-Origin', options.origin ?? '*');\n res.setHeader('Access-Control-Allow-Methods', ALLOWED_METHODS);\n res.setHeader('Access-Control-Allow-Headers', headers);\n if (options.credentials) res.setHeader('Access-Control-Allow-Credentials', 'true');\n if (req.method === 'OPTIONS') {\n res.status(204).end();\n return;\n }\n next();\n });\n}\n","import type { MaestroActorResolver } from '@maykonpaulo/maestro-core';\n\n/**\n * The default resolver used when `createMaestroServer` is called without one. It grants every request a\n * single admin actor — convenient for local development and demos, and **never** appropriate for\n * production. In production the consumer supplies its own resolver (deriving the actor from a JWT, cookie\n * or session), keeping authentication the consumer's responsibility (ADR 0008, DA-18).\n */\nexport const devActorResolver: MaestroActorResolver = async () => ({\n actor: {\n id: 'dev-admin',\n type: 'user',\n name: 'Development Admin',\n roles: ['admin'],\n },\n});\n","import {\n createMaestro,\n mergeIntrospectionWithOverrides,\n} from '@maykonpaulo/maestro-core';\nimport type {\n MaestroEngine,\n IntrospectionProvider,\n DatasourceProvider,\n EntityCapabilities,\n EntitySchema,\n RbacPolicy,\n LoadedConfig,\n OperationDef,\n AuditRepository,\n Logger,\n} from '@maykonpaulo/maestro-core';\n\n/** Access level applied to entities discovered by introspection. */\nexport type IntrospectedAccess = 'full' | 'readonly';\n\nexport interface CreateIntrospectedEngineOptions {\n /**\n * A live provider that can both describe the database (`introspect`) and operate it (`list/create/...`).\n * The MongoDB, SQL, and other official providers implement both interfaces on a single instance.\n */\n provider: IntrospectionProvider & DatasourceProvider;\n /** Datasource id the discovered entities are bound to. Defaults to `main`. */\n datasourceId?: string;\n /**\n * `full` (default) enables every capability on discovered entities, turning the engine into a complete\n * database manipulator. `readonly` enables only list/detail/export — a safe way to browse a database\n * without any risk of mutating it.\n */\n access?: IntrospectedAccess;\n /**\n * Optional curated overlay (labels, extra fields, relations, RBAC) merged over the introspected schema\n * via the core `HybridLoader`. Entities you curate here keep their own explicit `capabilities`.\n */\n overrides?: LoadedConfig;\n /**\n * RBAC policy for the engine. When omitted, a default `admin` role with `['*']` is used — appropriate\n * for a single platform operator and meant to be replaced with a real policy in multi-role deployments.\n */\n policies?: RbacPolicy;\n operations?: OperationDef[];\n audit?: AuditRepository;\n logger?: Logger;\n}\n\nconst FULL_CAPABILITIES: EntityCapabilities = {\n list: true,\n detail: true,\n create: true,\n update: true,\n clone: true,\n delete: true,\n softDelete: true,\n export: true,\n bulkActions: true,\n};\n\nconst READONLY_CAPABILITIES: Partial<EntityCapabilities> = {\n list: true,\n detail: true,\n create: false,\n update: false,\n clone: false,\n delete: false,\n softDelete: false,\n export: true,\n bulkActions: false,\n};\n\nconst DEFAULT_ADMIN_POLICY: RbacPolicy = {\n roles: {\n admin: { id: 'admin', name: 'Administrator', permissions: ['*'] },\n },\n};\n\n/**\n * Resolves the capabilities for one entity. Curated entities (an override that set `capabilities`\n * explicitly) are respected as-is; entities discovered by introspection get the access-level defaults.\n * The `softDelete` capability is only ever enabled when the entity actually declares a soft-delete field,\n * since the engine throws a configuration error otherwise.\n */\nfunction capabilitiesFor(entity: EntitySchema, access: IntrospectedAccess): Partial<EntityCapabilities> {\n if (entity.capabilities) return entity.capabilities;\n const base = access === 'full' ? FULL_CAPABILITIES : READONLY_CAPABILITIES;\n return { ...base, softDelete: access === 'full' ? Boolean(entity.softDelete) : false };\n}\n\n/**\n * Builds a `MaestroEngine` directly from a live database, with no per-entity declaration required\n * (ADR 0008, DA-17). It introspects the provider, turns every discovered collection/table into a\n * manipulable entity, and — unlike `createMaestroFromIntrospection`, whose entities are read-only by\n * default — enables write capabilities according to `access`, so \"point at the database and manipulate\n * everything\" works out of the box.\n *\n * Uses only public core exports (`introspect` → `mergeIntrospectionWithOverrides` → `createMaestro`).\n */\nexport async function createIntrospectedEngine(\n options: CreateIntrospectedEngineOptions,\n): Promise<MaestroEngine> {\n const datasourceId = options.datasourceId ?? 'main';\n const access = options.access ?? 'full';\n\n const introspection = await options.provider.introspect();\n const overrides: LoadedConfig = options.overrides ?? { entities: [], relations: [] };\n\n const config = mergeIntrospectionWithOverrides({\n introspection,\n overrides,\n datasources: { [datasourceId]: options.provider },\n defaultDatasource: datasourceId,\n strategy: 'extend',\n operations: options.operations,\n audit: options.audit,\n logger: options.logger,\n });\n\n config.entities = (config.entities ?? []).map((entity) => ({\n ...entity,\n capabilities: capabilitiesFor(entity, access),\n }));\n\n config.policies = options.policies ?? config.policies ?? DEFAULT_ADMIN_POLICY;\n\n return createMaestro(config);\n}\n"],"mappings":";AAAA,SAAS,oBAAiC;AAC1C,OAAO,aAA+B;AACtC,SAAS,iCAAiC;;;ACC1C,SAAS,iBAAiB,KAAkC;AAC1D,SAAO;AAAA,IACL,QAAQ,IAAI;AAAA,IACZ,OAAO,IAAI;AAAA,IACX,MAAM,IAAI;AAAA,IACV,SAAS,IAAI;AAAA,IACb,QAAQ,IAAI;AAAA,IACZ,MAAM,IAAI;AAAA,EACZ;AACF;AAMO,SAAS,MAAM,SAA6B;AACjD,SAAO,OAAO,KAAc,QAAiC;AAC3D,UAAM,WAAW,MAAM,QAAQ,iBAAiB,GAAG,CAAC;AAEpD,QAAI,SAAS,SAAS;AACpB,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,SAAS,OAAO,GAAG;AAC3D,YAAI,UAAU,KAAK,KAAK;AAAA,MAC1B;AAAA,IACF;AAEA,QAAI,OAAO,SAAS,MAAM;AAE1B,QAAI,SAAS,SAAS,QAAQ,SAAS,SAAS,QAAW;AACzD,UAAI,IAAI;AAAA,IACV,WAAW,OAAO,SAAS,SAAS,UAAU;AAC5C,UAAI,KAAK,SAAS,IAAI;AAAA,IACxB,OAAO;AACL,UAAI,KAAK,SAAS,IAAI;AAAA,IACxB;AAAA,EACF;AACF;;;AC3BA,IAAM,kBAAkB,CAAC,gBAAgB,iBAAiB,QAAQ;AAClE,IAAM,kBAAkB;AAMjB,SAAS,UAAU,KAAc,UAAuB,CAAC,GAAS;AACvE,QAAM,UAAU,CAAC,GAAG,iBAAiB,GAAI,QAAQ,kBAAkB,CAAC,CAAE,EAAE,KAAK,IAAI;AACjF,MAAI,IAAI,CAAC,KAAc,KAAe,SAA6B;AACjE,QAAI,UAAU,+BAA+B,QAAQ,UAAU,GAAG;AAClE,QAAI,UAAU,gCAAgC,eAAe;AAC7D,QAAI,UAAU,gCAAgC,OAAO;AACrD,QAAI,QAAQ,YAAa,KAAI,UAAU,oCAAoC,MAAM;AACjF,QAAI,IAAI,WAAW,WAAW;AAC5B,UAAI,OAAO,GAAG,EAAE,IAAI;AACpB;AAAA,IACF;AACA,SAAK;AAAA,EACP,CAAC;AACH;;;ACvBO,IAAM,mBAAyC,aAAa;AAAA,EACjE,OAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,IACN,OAAO,CAAC,OAAO;AAAA,EACjB;AACF;;;AHyBA,IAAM,SAAS;AAAA,EACb,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,gBAAgB;AAAA,EAChB,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,YAAY;AACd;AAOO,SAAS,oBAAoB,SAAoD;AACtF,QAAM,gBAAgB,QAAQ,iBAAiB;AAC/C,QAAM,WAAW,0BAA0B,QAAQ,QAAQ,EAAE,cAAc,CAAC;AAE5E,QAAM,MAAM,QAAQ,OAAO,QAAQ;AAEnC,MAAI,CAAC,QAAQ,KAAK;AAChB,QAAI,QAAQ,SAAS,OAAO;AAC1B,gBAAU,KAAK,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO,CAAC,CAAC;AAAA,IACrE;AACA,QAAI,IAAI,QAAQ,KAAK,CAAC;AAAA,EACxB;AAEA,QAAM,OAAO,QAAQ,YAAY;AACjC,QAAM,KAAK,CAAC,SAAyB,GAAG,IAAI,GAAG,IAAI;AAEnD,MAAI,IAAI,GAAG,OAAO,MAAM,GAAG,MAAM,SAAS,MAAM,CAAC;AACjD,MAAI,IAAI,GAAG,OAAO,QAAQ,GAAG,MAAM,SAAS,WAAW,CAAC;AACxD,MAAI,IAAI,GAAG,OAAO,cAAc,GAAG,MAAM,SAAS,iBAAiB,CAAC;AAEpE,MAAI,KAAK,GAAG,OAAO,SAAS,GAAG,MAAM,SAAS,gBAAgB,CAAC;AAI/D,MAAI,IAAI,GAAG,OAAO,MAAM,GAAG,MAAM,SAAS,MAAM,CAAC;AACjD,MAAI,IAAI,GAAG,OAAO,IAAI,GAAG,MAAM,SAAS,IAAI,CAAC;AAC7C,MAAI,KAAK,GAAG,OAAO,MAAM,GAAG,MAAM,SAAS,MAAM,CAAC;AAClD,MAAI,IAAI,GAAG,OAAO,QAAQ,GAAG,MAAM,SAAS,QAAQ,CAAC;AACrD,MAAI,MAAM,GAAG,OAAO,MAAM,GAAG,MAAM,SAAS,MAAM,CAAC;AACnD,MAAI,KAAK,GAAG,OAAO,KAAK,GAAG,MAAM,SAAS,KAAK,CAAC;AAChD,MAAI,KAAK,GAAG,OAAO,UAAU,GAAG,MAAM,SAAS,UAAU,CAAC;AAC1D,MAAI,KAAK,GAAG,OAAO,OAAO,GAAG,MAAM,SAAS,OAAO,CAAC;AACpD,MAAI,OAAO,GAAG,OAAO,UAAU,GAAG,MAAM,SAAS,UAAU,CAAC;AAE5D,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,OAAO,MAAc,MAAgC;AACnD,YAAM,SAAS,aAAa,GAAG;AAC/B,aAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,eAAO,KAAK,SAAS,MAAM;AAC3B,cAAM,UAAU,MAAY;AAC1B,iBAAO,IAAI,SAAS,MAAM;AAC1B,kBAAQ,MAAM;AAAA,QAChB;AACA,YAAI,KAAM,QAAO,OAAO,MAAM,MAAM,OAAO;AAAA,YACtC,QAAO,OAAO,MAAM,OAAO;AAAA,MAClC,CAAC;AAAA,IACH;AAAA,EACF;AACF;;;AI/GA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AA8CP,IAAM,oBAAwC;AAAA,EAC5C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,aAAa;AACf;AAEA,IAAM,wBAAqD;AAAA,EACzD,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,aAAa;AACf;AAEA,IAAM,uBAAmC;AAAA,EACvC,OAAO;AAAA,IACL,OAAO,EAAE,IAAI,SAAS,MAAM,iBAAiB,aAAa,CAAC,GAAG,EAAE;AAAA,EAClE;AACF;AAQA,SAAS,gBAAgB,QAAsB,QAAyD;AACtG,MAAI,OAAO,aAAc,QAAO,OAAO;AACvC,QAAM,OAAO,WAAW,SAAS,oBAAoB;AACrD,SAAO,EAAE,GAAG,MAAM,YAAY,WAAW,SAAS,QAAQ,OAAO,UAAU,IAAI,MAAM;AACvF;AAWA,eAAsB,yBACpB,SACwB;AACxB,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,SAAS,QAAQ,UAAU;AAEjC,QAAM,gBAAgB,MAAM,QAAQ,SAAS,WAAW;AACxD,QAAM,YAA0B,QAAQ,aAAa,EAAE,UAAU,CAAC,GAAG,WAAW,CAAC,EAAE;AAEnF,QAAM,SAAS,gCAAgC;AAAA,IAC7C;AAAA,IACA;AAAA,IACA,aAAa,EAAE,CAAC,YAAY,GAAG,QAAQ,SAAS;AAAA,IAChD,mBAAmB;AAAA,IACnB,UAAU;AAAA,IACV,YAAY,QAAQ;AAAA,IACpB,OAAO,QAAQ;AAAA,IACf,QAAQ,QAAQ;AAAA,EAClB,CAAC;AAED,SAAO,YAAY,OAAO,YAAY,CAAC,GAAG,IAAI,CAAC,YAAY;AAAA,IACzD,GAAG;AAAA,IACH,cAAc,gBAAgB,QAAQ,MAAM;AAAA,EAC9C,EAAE;AAEF,SAAO,WAAW,QAAQ,YAAY,OAAO,YAAY;AAEzD,SAAO,cAAc,MAAM;AAC7B;","names":[]}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@maykonpaulo/maestro-server",
3
+ "version": "0.1.0-next.1",
4
+ "description": "Turnkey HTTP server for @maykonpaulo/maestro-core — mounts the framework-agnostic Maestro HTTP handlers on a real Express server and turns any live datasource provider into a full, governed admin over the whole database (ADR 0008). The core stays server-free; this sibling package is the batteries-included layer.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "peerDependencies": {
23
+ "@maykonpaulo/maestro-core": "0.7.0-next.2"
24
+ },
25
+ "dependencies": {
26
+ "express": "^4.19.2"
27
+ },
28
+ "devDependencies": {
29
+ "@types/express": "^4.17.21",
30
+ "@types/node": "^22.0.0",
31
+ "mongodb": "^7.4.0",
32
+ "mongodb-memory-server": "^11.2.0",
33
+ "rimraf": "^6.0.0",
34
+ "tsup": "^8.0.0",
35
+ "typescript": "^5.6.0",
36
+ "vitest": "^2.0.0",
37
+ "@maykonpaulo/maestro-core": "0.7.0-next.2",
38
+ "@maykonpaulo/maestro-provider-mongodb": "1.0.0-next.1"
39
+ },
40
+ "scripts": {
41
+ "build": "tsup",
42
+ "test": "vitest run",
43
+ "test:integration": "vitest run --config vitest.integration.config.ts",
44
+ "typecheck": "tsc --noEmit",
45
+ "lint": "eslint src tests",
46
+ "clean": "rimraf dist"
47
+ }
48
+ }