@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 +33 -2
- package/dist/index.js +200 -0
- package/package.json +54 -54
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
|
-
|
|
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.
|
|
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
|
+
}
|