@ensera/plugin-backend 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Request, Response, NextFunction } from 'express';
1
+ import { Request, Response, NextFunction, Express } from 'express';
2
2
 
3
3
  /**
4
4
  * Plugin scope extracted from JWT token
@@ -373,4 +373,35 @@ declare function toDateString(date: string | Date | null | undefined): string |
373
373
  */
374
374
  declare function buildDateTime(dateValue: string | Date | null | undefined, timeValue: string | null | undefined, defaultHour?: number, defaultMinute?: number): Date | null;
375
375
 
376
- export { type BackendNotification, type JsonObject, type JsonValue, type NotificationAction, type NotificationApiPayload, type NotificationBulkApiPayload, type NotificationBulkResponse, type NotificationCancelResponse, type NotificationCapabilities, type NotificationOptions, type NotificationPublishResponse, type NotificationPublisher, type NotificationPublisherConfig, type NotificationTypeConfig, type PluginAuthOptions, type PluginContext, PluginError, type PluginErrorResponse, type PluginScope, type RequestWithContext, type ScheduleOptions, assertPluginScope, buildDateTime, calculateScheduleTime, createNotificationPublisher, forbidClientInstanceId, formatTime, getPluginScope, isPastDate, pluginAuth, pluginErrorHandler, requireFeature, requirePermission, requireTabView, requireTaskScope, resolveEffectiveInstanceId, toDateString, withInstanceOnly, withInstanceScope, withUserAndInstance };
376
+ declare function withCoreServiceToken(handler: (req: Request, res: Response, next: NextFunction) => unknown): (req: Request, res: Response, next: NextFunction) => Promise<void>;
377
+
378
+ type CoreRequestContext = {
379
+ workspaceId: string;
380
+ spaceId: string;
381
+ userId: string;
382
+ taskScopeId?: string;
383
+ requestId: string;
384
+ };
385
+ type CommandHandler = (input: Record<string, unknown>, context: CoreRequestContext) => Promise<unknown>;
386
+ type CommandRegistry = {
387
+ register: (name: string, handler: CommandHandler) => void;
388
+ get: (name: string) => CommandHandler | undefined;
389
+ list: () => string[];
390
+ };
391
+ declare function createCommandRegistry(): CommandRegistry;
392
+ declare function createCoreRouter(app: Express, registry: CommandRegistry): void;
393
+
394
+ type CreateDbOptions = {
395
+ prismaDir?: string;
396
+ coreApiUrl?: string;
397
+ serviceToken?: string;
398
+ runMigrations?: boolean;
399
+ };
400
+ type DbProvisionResult = {
401
+ connectionString: string;
402
+ schemaName: string;
403
+ featureSlug: string;
404
+ };
405
+ declare function provisionDb(options?: CreateDbOptions): Promise<DbProvisionResult>;
406
+
407
+ export { type BackendNotification, type CommandHandler, type CommandRegistry, type CoreRequestContext, type CreateDbOptions, type DbProvisionResult, type JsonObject, type JsonValue, type NotificationAction, type NotificationApiPayload, type NotificationBulkApiPayload, type NotificationBulkResponse, type NotificationCancelResponse, type NotificationCapabilities, type NotificationOptions, type NotificationPublishResponse, type NotificationPublisher, type NotificationPublisherConfig, type NotificationTypeConfig, type PluginAuthOptions, type PluginContext, PluginError, type PluginErrorResponse, type PluginScope, type RequestWithContext, type ScheduleOptions, assertPluginScope, buildDateTime, calculateScheduleTime, createCommandRegistry, createCoreRouter, createNotificationPublisher, forbidClientInstanceId, formatTime, getPluginScope, isPastDate, pluginAuth, pluginErrorHandler, provisionDb, requireFeature, requirePermission, requireTabView, requireTaskScope, resolveEffectiveInstanceId, toDateString, withCoreServiceToken, withInstanceOnly, withInstanceScope, withUserAndInstance };
package/dist/index.js CHANGED
@@ -657,11 +657,209 @@ function buildDateTime(dateValue, timeValue, defaultHour = 9, defaultMinute = 0)
657
657
  if (isNaN(result.getTime())) return null;
658
658
  return result;
659
659
  }
660
+
661
+ // src/withCoreServiceToken.ts
662
+ function withCoreServiceToken(handler) {
663
+ return async function coreServiceTokenWrapped(req, res, next) {
664
+ try {
665
+ const authHeader = req.headers.authorization;
666
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
667
+ const expected = process.env.CORE_SERVICE_TOKEN?.trim();
668
+ if (!expected) {
669
+ throw new PluginError({
670
+ status: 500,
671
+ code: "CORE_SERVICE_TOKEN_NOT_CONFIGURED",
672
+ message: "CORE_SERVICE_TOKEN is not set in environment"
673
+ });
674
+ }
675
+ if (!token || token !== expected) {
676
+ throw new PluginError({
677
+ status: 401,
678
+ code: "INVALID_CORE_SERVICE_TOKEN",
679
+ message: "Unauthorized: invalid or missing Core service token"
680
+ });
681
+ }
682
+ await handler(req, res, next);
683
+ } catch (e) {
684
+ next(e);
685
+ }
686
+ };
687
+ }
688
+
689
+ // src/createCommandRegistry.ts
690
+ function createCommandRegistry() {
691
+ const registry = /* @__PURE__ */ new Map();
692
+ return {
693
+ register(name, handler) {
694
+ if (!name || typeof handler !== "function") {
695
+ throw new Error(
696
+ "createCommandRegistry: name and handler are required"
697
+ );
698
+ }
699
+ registry.set(name, handler);
700
+ },
701
+ get(name) {
702
+ return registry.get(name);
703
+ },
704
+ list() {
705
+ return Array.from(registry.keys());
706
+ }
707
+ };
708
+ }
709
+ function createCoreRouter(app, registry) {
710
+ app.post(
711
+ "/api/core/execute",
712
+ withCoreServiceToken(async (req, res) => {
713
+ const { command, input, context } = req.body ?? {};
714
+ if (!command || typeof command !== "string") {
715
+ return res.status(400).json({
716
+ success: false,
717
+ error: "MISSING_COMMAND",
718
+ message: "Request body must include a command string"
719
+ });
720
+ }
721
+ if (!context?.workspaceId || !context?.userId) {
722
+ return res.status(400).json({
723
+ success: false,
724
+ error: "MISSING_CONTEXT",
725
+ message: "Request context must include workspaceId and userId"
726
+ });
727
+ }
728
+ const handler = registry.get(command);
729
+ if (!handler) {
730
+ return res.status(404).json({
731
+ success: false,
732
+ error: "UNKNOWN_COMMAND",
733
+ message: `Command '${command}' is not registered`,
734
+ available: registry.list()
735
+ });
736
+ }
737
+ try {
738
+ const result = await handler(
739
+ input ?? {},
740
+ context
741
+ );
742
+ return res.json({ success: true, data: result });
743
+ } catch (err) {
744
+ console.error(`[CoreRouter] Command '${command}' failed:`, err);
745
+ return res.status(500).json({
746
+ success: false,
747
+ error: "COMMAND_FAILED",
748
+ message: err?.message ?? "Internal error"
749
+ });
750
+ }
751
+ })
752
+ );
753
+ }
754
+
755
+ // src/createDb.ts
756
+ import { execSync } from "child_process";
757
+ import fs from "fs";
758
+ import path from "path";
759
+ function isRecord(value) {
760
+ return typeof value === "object" && value !== null && !Array.isArray(value);
761
+ }
762
+ function getString(value) {
763
+ if (typeof value !== "string") return void 0;
764
+ const trimmed = value.trim();
765
+ return trimmed.length > 0 ? trimmed : void 0;
766
+ }
767
+ function parseProvisionResult(value) {
768
+ if (!isRecord(value)) {
769
+ throw new Error("provisionDb: Core returned an invalid response body");
770
+ }
771
+ const connectionString = getString(value.connectionString);
772
+ const schemaName = getString(value.schemaName);
773
+ const featureSlug = getString(value.featureSlug);
774
+ if (!connectionString) {
775
+ throw new Error("provisionDb: Core did not return a connectionString");
776
+ }
777
+ if (!schemaName) {
778
+ throw new Error("provisionDb: Core did not return a schemaName");
779
+ }
780
+ if (!featureSlug) {
781
+ throw new Error("provisionDb: Core did not return a featureSlug");
782
+ }
783
+ return {
784
+ connectionString,
785
+ schemaName,
786
+ featureSlug
787
+ };
788
+ }
789
+ async function provisionDb(options = {}) {
790
+ const coreApiUrl = (options.coreApiUrl ?? process.env.CORE_API_URL)?.replace(/\/+$/, "");
791
+ const serviceToken = options.serviceToken ?? process.env.CORE_SERVICE_TOKEN;
792
+ if (!coreApiUrl) {
793
+ throw new Error(
794
+ "provisionDb: CORE_API_URL is not set. Add it to your .env file."
795
+ );
796
+ }
797
+ if (!serviceToken) {
798
+ throw new Error(
799
+ "provisionDb: CORE_SERVICE_TOKEN is not set. Add it to your .env file."
800
+ );
801
+ }
802
+ const response = await fetch(`${coreApiUrl}/api/internal/db/provision`, {
803
+ method: "POST",
804
+ headers: {
805
+ "Content-Type": "application/json",
806
+ Authorization: `Bearer ${serviceToken}`
807
+ }
808
+ });
809
+ let payload = null;
810
+ try {
811
+ payload = await response.json();
812
+ } catch {
813
+ payload = null;
814
+ }
815
+ if (!response.ok) {
816
+ const errorMessage = isRecord(payload) ? getString(payload.error) : void 0;
817
+ throw new Error(
818
+ `provisionDb: Core returned ${response.status}. ${errorMessage ?? "Unknown error"}`
819
+ );
820
+ }
821
+ const result = parseProvisionResult(payload);
822
+ const shouldMigrate = options.runMigrations !== false;
823
+ if (shouldMigrate) {
824
+ const prismaDir = options.prismaDir ?? path.join(process.cwd(), "prisma");
825
+ const migrationsDir = path.join(prismaDir, "migrations");
826
+ if (fs.existsSync(prismaDir) && fs.existsSync(migrationsDir)) {
827
+ console.log(
828
+ `[ensera-db] Running migrations for schema: ${result.schemaName}`
829
+ );
830
+ try {
831
+ execSync("npx prisma migrate deploy", {
832
+ cwd: path.dirname(prismaDir),
833
+ stdio: "inherit",
834
+ env: {
835
+ ...process.env,
836
+ DATABASE_URL: result.connectionString
837
+ }
838
+ });
839
+ console.log("[ensera-db] Migrations complete");
840
+ } catch (error) {
841
+ const message = error instanceof Error ? error.message : "Unknown migration error";
842
+ throw new Error(`[ensera-db] Migration failed: ${message}`);
843
+ }
844
+ } else if (fs.existsSync(prismaDir)) {
845
+ console.warn(
846
+ `[ensera-db] No prisma migrations found at ${migrationsDir}. Skipping migrations.`
847
+ );
848
+ } else {
849
+ console.warn(
850
+ `[ensera-db] No prisma directory found at ${prismaDir}. Skipping migrations.`
851
+ );
852
+ }
853
+ }
854
+ return result;
855
+ }
660
856
  export {
661
857
  PluginError,
662
858
  assertPluginScope,
663
859
  buildDateTime,
664
860
  calculateScheduleTime,
861
+ createCommandRegistry,
862
+ createCoreRouter,
665
863
  createNotificationPublisher,
666
864
  forbidClientInstanceId,
667
865
  formatTime,
@@ -669,12 +867,14 @@ export {
669
867
  isPastDate,
670
868
  pluginAuth,
671
869
  pluginErrorHandler,
870
+ provisionDb,
672
871
  requireFeature,
673
872
  requirePermission,
674
873
  requireTabView,
675
874
  requireTaskScope,
676
875
  resolveEffectiveInstanceId,
677
876
  toDateString,
877
+ withCoreServiceToken,
678
878
  withInstanceOnly,
679
879
  withInstanceScope,
680
880
  withUserAndInstance
package/package.json CHANGED
@@ -1,54 +1,54 @@
1
- {
2
- "name": "@ensera/plugin-backend",
3
- "version": "1.0.0",
4
- "description": "Runtime backend SDK for Ensera plugins.",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "files": [
10
- "dist",
11
- "README.md"
12
- ],
13
- "sideEffects": false,
14
- "scripts": {
15
- "build": "tsup src/index.ts --format esm --dts --clean --target es2022 --outDir dist",
16
- "dev": "tsup src/index.ts --format esm --dts --watch --target es2022 --outDir dist",
17
- "typecheck": "tsc -p tsconfig.json --noEmit",
18
- "prepublishOnly": "npm run build && npm run typecheck"
19
- },
20
- "keywords": [
21
- "ensera",
22
- "backend",
23
- "sdk",
24
- "plugin",
25
- "express"
26
- ],
27
- "publishConfig": {
28
- "access": "public"
29
- },
30
- "dependencies": {
31
- "jsonwebtoken": "^9.0.0"
32
- },
33
- "devDependencies": {
34
- "@types/express": "^4.17.0",
35
- "@types/jsonwebtoken": "^9.0.0",
36
- "tsup": "^8.0.0",
37
- "typescript": "^5.0.0",
38
- "jsonwebtoken": "^9.0.0"
39
- },
40
- "engines": {
41
- "node": ">=18"
42
- },
43
- "peerDependencies": {
44
- "express": "^4.18.0",
45
- "jsonwebtoken": "^9.0.0"
46
- },
47
- "exports": {
48
- ".": {
49
- "types": "./dist/index.d.ts",
50
- "import": "./dist/index.js"
51
- },
52
- "./package.json": "./package.json"
53
- }
54
- }
1
+ {
2
+ "name": "@ensera/plugin-backend",
3
+ "version": "1.2.0",
4
+ "description": "Runtime backend SDK for Ensera plugins.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "sideEffects": false,
14
+ "scripts": {
15
+ "build": "tsup src/index.ts --format esm --dts --clean --target es2022 --outDir dist",
16
+ "dev": "tsup src/index.ts --format esm --dts --watch --target es2022 --outDir dist",
17
+ "typecheck": "tsc -p tsconfig.json --noEmit",
18
+ "prepublishOnly": "npm run build && npm run typecheck"
19
+ },
20
+ "keywords": [
21
+ "ensera",
22
+ "backend",
23
+ "sdk",
24
+ "plugin",
25
+ "express"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "dependencies": {
31
+ "jsonwebtoken": "^9.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/express": "^4.17.0",
35
+ "@types/jsonwebtoken": "^9.0.0",
36
+ "tsup": "^8.0.0",
37
+ "typescript": "^5.0.0",
38
+ "jsonwebtoken": "^9.0.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "peerDependencies": {
44
+ "express": "^4.18.0",
45
+ "jsonwebtoken": "^9.0.0"
46
+ },
47
+ "exports": {
48
+ ".": {
49
+ "types": "./dist/index.d.ts",
50
+ "import": "./dist/index.js"
51
+ },
52
+ "./package.json": "./package.json"
53
+ }
54
+ }