@usebetterdev/plugin 0.6.0 → 0.7.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.
@@ -0,0 +1,23 @@
1
+ import type { BetterConfig } from "./config-types.js";
2
+ /** Thrown when no `better.config.*` file is found in the working directory. */
3
+ export declare class BetterConfigNotFoundError extends Error {
4
+ readonly name = "BetterConfigNotFoundError";
5
+ readonly cwd: string;
6
+ constructor(cwd: string);
7
+ }
8
+ export interface LoadBetterConfigOptions {
9
+ /** Working directory to search for the config file. Defaults to `process.cwd()`. */
10
+ cwd?: string;
11
+ /** Explicit path to a config file, bypassing discovery. Maps to c12's `configFile` option. */
12
+ configPath?: string;
13
+ }
14
+ /**
15
+ * Loads a `better.config.{ts,js,mjs}` file using c12.
16
+ * Throws {@link BetterConfigNotFoundError} when no config file is found.
17
+ *
18
+ * The returned config is **unvalidated**. Pass it through
19
+ * {@link requireTenantConfig} or {@link requireAuditConfig} to validate
20
+ * product-specific sections.
21
+ */
22
+ export declare function loadBetterConfig(options?: LoadBetterConfigOptions): Promise<BetterConfig>;
23
+ //# sourceMappingURL=config-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-loader.d.ts","sourceRoot":"","sources":["../src/config-loader.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,+EAA+E;AAC/E,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,SAAkB,IAAI,+BAA+B;IACrD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;gBAET,GAAG,EAAE,MAAM;CAgBxB;AAED,MAAM,WAAW,uBAAuB;IACtC,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8FAA8F;IAC9F,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,YAAY,CAAC,CAsBvB"}
@@ -0,0 +1,2 @@
1
+ export type { TelemetryConfig, PathResolverConfig, SubdomainResolverConfig, StaticJwtResolverConfig, StaticTenantResolverConfig, BetterTenantStaticConfig, RetentionPolicy, BetterAuditStaticConfig, StaticMagicLinkConfig, StaticConsoleSessionConfig, BetterConsoleStaticConfig, BetterConfig, } from "@usebetterdev/contract/config-types";
2
+ //# sourceMappingURL=config-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-types.d.ts","sourceRoot":"","sources":["../src/config-types.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,kBAAkB,EAClB,uBAAuB,EACvB,uBAAuB,EACvB,0BAA0B,EAC1B,wBAAwB,EACxB,eAAe,EACf,uBAAuB,EACvB,qBAAqB,EACrB,0BAA0B,EAC1B,yBAAyB,EACzB,YAAY,GACb,MAAM,qCAAqC,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { BetterConfig, BetterTenantStaticConfig, BetterAuditStaticConfig } from "./config-types.js";
2
+ /** Thrown when a config section fails validation. */
3
+ export declare class ConfigValidationError extends Error {
4
+ readonly name = "ConfigValidationError";
5
+ readonly field: string;
6
+ constructor(field: string, message: string);
7
+ }
8
+ /**
9
+ * Validates that `config.tenant` exists and its `tenantTables` is a non-empty
10
+ * array of non-empty strings. Returns the narrowed tenant config.
11
+ */
12
+ export declare function requireTenantConfig(config: BetterConfig): BetterTenantStaticConfig;
13
+ /**
14
+ * Validates that `config.audit` exists and its `auditTables` is a non-empty
15
+ * array of non-empty strings. When `retention` is present, validates `days` is
16
+ * a positive integer and `tables` (if present) is a non-empty string array.
17
+ */
18
+ export declare function requireAuditConfig(config: BetterConfig): BetterAuditStaticConfig;
19
+ //# sourceMappingURL=config-validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-validation.d.ts","sourceRoot":"","sources":["../src/config-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,mBAAmB,CAAC;AAE3B,qDAAqD;AACrD,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,SAAkB,IAAI,2BAA2B;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;gBAEX,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAI3C;AA4BD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,YAAY,GACnB,wBAAwB,CAM1B;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,GACnB,uBAAuB,CAoBzB"}
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/config.ts
21
+ var config_exports = {};
22
+ __export(config_exports, {
23
+ BetterConfigNotFoundError: () => BetterConfigNotFoundError,
24
+ ConfigValidationError: () => ConfigValidationError,
25
+ defineBetterConfig: () => defineBetterConfig,
26
+ loadBetterConfig: () => loadBetterConfig,
27
+ requireAuditConfig: () => requireAuditConfig,
28
+ requireTenantConfig: () => requireTenantConfig
29
+ });
30
+ module.exports = __toCommonJS(config_exports);
31
+
32
+ // src/define-config.ts
33
+ function defineBetterConfig(config) {
34
+ return config;
35
+ }
36
+
37
+ // src/config-loader.ts
38
+ var import_c12 = require("c12");
39
+ var BetterConfigNotFoundError = class extends Error {
40
+ name = "BetterConfigNotFoundError";
41
+ cwd;
42
+ constructor(cwd) {
43
+ super(
44
+ [
45
+ `No better.config.{ts,js,mjs} file found in ${cwd}.`,
46
+ "",
47
+ "Create one with:",
48
+ "",
49
+ ' import { defineBetterConfig } from "@usebetterdev/plugin/config";',
50
+ "",
51
+ " export default defineBetterConfig({",
52
+ ' tenant: { tenantTables: ["organizations"] },',
53
+ " });"
54
+ ].join("\n")
55
+ );
56
+ this.cwd = cwd;
57
+ }
58
+ };
59
+ async function loadBetterConfig(options) {
60
+ const cwd = options?.cwd ?? process.cwd();
61
+ const configFile = options?.configPath;
62
+ const result = await (0, import_c12.loadConfig)({
63
+ name: "better",
64
+ cwd,
65
+ ...configFile !== void 0 ? { configFile } : {},
66
+ rcFile: false,
67
+ globalRc: false,
68
+ packageJson: false,
69
+ dotenv: false
70
+ });
71
+ const layers = result.layers ?? [];
72
+ if (layers.length === 0) {
73
+ throw new BetterConfigNotFoundError(cwd);
74
+ }
75
+ return result.config ?? {};
76
+ }
77
+
78
+ // src/config-validation.ts
79
+ var ConfigValidationError = class extends Error {
80
+ name = "ConfigValidationError";
81
+ field;
82
+ constructor(field, message) {
83
+ super(`Config error: "${field}" ${message}`);
84
+ this.field = field;
85
+ }
86
+ };
87
+ function isNonEmptyString(value) {
88
+ return typeof value === "string" && value.length > 0;
89
+ }
90
+ function validateStringArray(items, fieldName, sectionName) {
91
+ const qualifiedName = `${sectionName}.${fieldName}`;
92
+ if (!Array.isArray(items)) {
93
+ throw new ConfigValidationError(qualifiedName, "must be an array");
94
+ }
95
+ if (items.length === 0) {
96
+ throw new ConfigValidationError(qualifiedName, "must not be empty");
97
+ }
98
+ for (const item of items) {
99
+ if (!isNonEmptyString(item)) {
100
+ throw new ConfigValidationError(
101
+ qualifiedName,
102
+ "entries must be non-empty strings"
103
+ );
104
+ }
105
+ }
106
+ }
107
+ function requireTenantConfig(config) {
108
+ if (config.tenant === void 0) {
109
+ throw new ConfigValidationError("tenant", "section is required");
110
+ }
111
+ validateStringArray(config.tenant.tenantTables, "tenantTables", "tenant");
112
+ return config.tenant;
113
+ }
114
+ function requireAuditConfig(config) {
115
+ if (config.audit === void 0) {
116
+ throw new ConfigValidationError("audit", "section is required");
117
+ }
118
+ validateStringArray(config.audit.auditTables, "auditTables", "audit");
119
+ if (config.audit.retention !== void 0) {
120
+ const { days, tables } = config.audit.retention;
121
+ if (!Number.isInteger(days) || days <= 0) {
122
+ throw new ConfigValidationError(
123
+ "audit.retention.days",
124
+ "must be a positive integer"
125
+ );
126
+ }
127
+ if (tables !== void 0) {
128
+ validateStringArray(tables, "retention.tables", "audit");
129
+ }
130
+ }
131
+ return config.audit;
132
+ }
133
+ // Annotate the CommonJS export names for ESM import in node:
134
+ 0 && (module.exports = {
135
+ BetterConfigNotFoundError,
136
+ ConfigValidationError,
137
+ defineBetterConfig,
138
+ loadBetterConfig,
139
+ requireAuditConfig,
140
+ requireTenantConfig
141
+ });
142
+ //# sourceMappingURL=config.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/define-config.ts","../src/config-loader.ts","../src/config-validation.ts"],"sourcesContent":["// ── Config helpers ───────────────────────────────────────────────────────────\n\nexport { defineBetterConfig } from \"./define-config.js\";\n\nexport {\n loadBetterConfig,\n BetterConfigNotFoundError,\n} from \"./config-loader.js\";\nexport type { LoadBetterConfigOptions } from \"./config-loader.js\";\n\nexport {\n requireTenantConfig,\n requireAuditConfig,\n ConfigValidationError,\n} from \"./config-validation.js\";\n\n// ── Re-export config types ──────────────────────────────────────────────────\n\nexport type {\n BetterConfig,\n BetterTenantStaticConfig,\n BetterAuditStaticConfig,\n BetterConsoleStaticConfig,\n TelemetryConfig,\n PathResolverConfig,\n SubdomainResolverConfig,\n StaticJwtResolverConfig,\n StaticTenantResolverConfig,\n RetentionPolicy,\n StaticMagicLinkConfig,\n StaticConsoleSessionConfig,\n} from \"./config-types.js\";\n","import type { BetterConfig } from \"./config-types.js\";\n\n/**\n * Identity helper that provides TypeScript autocomplete for `better.config.ts`.\n *\n * ```ts\n * // better.config.ts\n * import { defineBetterConfig } from \"@usebetterdev/plugin/config\";\n *\n * export default defineBetterConfig({\n * tenant: { tenantTables: [\"organizations\"] },\n * });\n * ```\n */\nexport function defineBetterConfig(config: BetterConfig): BetterConfig {\n return config;\n}\n","import { loadConfig } from \"c12\";\nimport type { BetterConfig } from \"./config-types.js\";\n\n/** Thrown when no `better.config.*` file is found in the working directory. */\nexport class BetterConfigNotFoundError extends Error {\n override readonly name = \"BetterConfigNotFoundError\";\n readonly cwd: string;\n\n constructor(cwd: string) {\n super(\n [\n `No better.config.{ts,js,mjs} file found in ${cwd}.`,\n \"\",\n \"Create one with:\",\n \"\",\n ' import { defineBetterConfig } from \"@usebetterdev/plugin/config\";',\n \"\",\n \" export default defineBetterConfig({\",\n ' tenant: { tenantTables: [\"organizations\"] },',\n \" });\",\n ].join(\"\\n\"),\n );\n this.cwd = cwd;\n }\n}\n\nexport interface LoadBetterConfigOptions {\n /** Working directory to search for the config file. Defaults to `process.cwd()`. */\n cwd?: string;\n /** Explicit path to a config file, bypassing discovery. Maps to c12's `configFile` option. */\n configPath?: string;\n}\n\n/**\n * Loads a `better.config.{ts,js,mjs}` file using c12.\n * Throws {@link BetterConfigNotFoundError} when no config file is found.\n *\n * The returned config is **unvalidated**. Pass it through\n * {@link requireTenantConfig} or {@link requireAuditConfig} to validate\n * product-specific sections.\n */\nexport async function loadBetterConfig(\n options?: LoadBetterConfigOptions,\n): Promise<BetterConfig> {\n const cwd = options?.cwd ?? process.cwd();\n\n // configPath maps to c12's configFile — spread conditionally to satisfy exactOptionalPropertyTypes\n const configFile = options?.configPath;\n\n const result = await loadConfig<BetterConfig>({\n name: \"better\",\n cwd,\n ...(configFile !== undefined ? { configFile } : {}),\n rcFile: false,\n globalRc: false,\n packageJson: false,\n dotenv: false,\n });\n\n const layers = result.layers ?? [];\n if (layers.length === 0) {\n throw new BetterConfigNotFoundError(cwd);\n }\n\n return result.config ?? {};\n}\n","import type {\n BetterConfig,\n BetterTenantStaticConfig,\n BetterAuditStaticConfig,\n} from \"./config-types.js\";\n\n/** Thrown when a config section fails validation. */\nexport class ConfigValidationError extends Error {\n override readonly name = \"ConfigValidationError\";\n readonly field: string;\n\n constructor(field: string, message: string) {\n super(`Config error: \"${field}\" ${message}`);\n this.field = field;\n }\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n\nfunction validateStringArray(\n items: unknown,\n fieldName: string,\n sectionName: string,\n): void {\n const qualifiedName = `${sectionName}.${fieldName}`;\n if (!Array.isArray(items)) {\n throw new ConfigValidationError(qualifiedName, \"must be an array\");\n }\n if (items.length === 0) {\n throw new ConfigValidationError(qualifiedName, \"must not be empty\");\n }\n for (const item of items) {\n if (!isNonEmptyString(item)) {\n throw new ConfigValidationError(\n qualifiedName,\n \"entries must be non-empty strings\",\n );\n }\n }\n}\n\n/**\n * Validates that `config.tenant` exists and its `tenantTables` is a non-empty\n * array of non-empty strings. Returns the narrowed tenant config.\n */\nexport function requireTenantConfig(\n config: BetterConfig,\n): BetterTenantStaticConfig {\n if (config.tenant === undefined) {\n throw new ConfigValidationError(\"tenant\", \"section is required\");\n }\n validateStringArray(config.tenant.tenantTables, \"tenantTables\", \"tenant\");\n return config.tenant;\n}\n\n/**\n * Validates that `config.audit` exists and its `auditTables` is a non-empty\n * array of non-empty strings. When `retention` is present, validates `days` is\n * a positive integer and `tables` (if present) is a non-empty string array.\n */\nexport function requireAuditConfig(\n config: BetterConfig,\n): BetterAuditStaticConfig {\n if (config.audit === undefined) {\n throw new ConfigValidationError(\"audit\", \"section is required\");\n }\n validateStringArray(config.audit.auditTables, \"auditTables\", \"audit\");\n\n if (config.audit.retention !== undefined) {\n const { days, tables } = config.audit.retention;\n if (!Number.isInteger(days) || days <= 0) {\n throw new ConfigValidationError(\n \"audit.retention.days\",\n \"must be a positive integer\",\n );\n }\n if (tables !== undefined) {\n validateStringArray(tables, \"retention.tables\", \"audit\");\n }\n }\n\n return config.audit;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACcO,SAAS,mBAAmB,QAAoC;AACrE,SAAO;AACT;;;AChBA,iBAA2B;AAIpB,IAAM,4BAAN,cAAwC,MAAM;AAAA,EACjC,OAAO;AAAA,EAChB;AAAA,EAET,YAAY,KAAa;AACvB;AAAA,MACE;AAAA,QACE,8CAA8C,GAAG;AAAA,QACjD;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,IAAI;AAAA,IACb;AACA,SAAK,MAAM;AAAA,EACb;AACF;AAiBA,eAAsB,iBACpB,SACuB;AACvB,QAAM,MAAM,SAAS,OAAO,QAAQ,IAAI;AAGxC,QAAM,aAAa,SAAS;AAE5B,QAAM,SAAS,UAAM,uBAAyB;AAAA,IAC5C,MAAM;AAAA,IACN;AAAA,IACA,GAAI,eAAe,SAAY,EAAE,WAAW,IAAI,CAAC;AAAA,IACjD,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,aAAa;AAAA,IACb,QAAQ;AAAA,EACV,CAAC;AAED,QAAM,SAAS,OAAO,UAAU,CAAC;AACjC,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,0BAA0B,GAAG;AAAA,EACzC;AAEA,SAAO,OAAO,UAAU,CAAC;AAC3B;;;AC1DO,IAAM,wBAAN,cAAoC,MAAM;AAAA,EAC7B,OAAO;AAAA,EAChB;AAAA,EAET,YAAY,OAAe,SAAiB;AAC1C,UAAM,kBAAkB,KAAK,KAAK,OAAO,EAAE;AAC3C,SAAK,QAAQ;AAAA,EACf;AACF;AAEA,SAAS,iBAAiB,OAAiC;AACzD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS;AACrD;AAEA,SAAS,oBACP,OACA,WACA,aACM;AACN,QAAM,gBAAgB,GAAG,WAAW,IAAI,SAAS;AACjD,MAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,UAAM,IAAI,sBAAsB,eAAe,kBAAkB;AAAA,EACnE;AACA,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,IAAI,sBAAsB,eAAe,mBAAmB;AAAA,EACpE;AACA,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,iBAAiB,IAAI,GAAG;AAC3B,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAMO,SAAS,oBACd,QAC0B;AAC1B,MAAI,OAAO,WAAW,QAAW;AAC/B,UAAM,IAAI,sBAAsB,UAAU,qBAAqB;AAAA,EACjE;AACA,sBAAoB,OAAO,OAAO,cAAc,gBAAgB,QAAQ;AACxE,SAAO,OAAO;AAChB;AAOO,SAAS,mBACd,QACyB;AACzB,MAAI,OAAO,UAAU,QAAW;AAC9B,UAAM,IAAI,sBAAsB,SAAS,qBAAqB;AAAA,EAChE;AACA,sBAAoB,OAAO,MAAM,aAAa,eAAe,OAAO;AAEpE,MAAI,OAAO,MAAM,cAAc,QAAW;AACxC,UAAM,EAAE,MAAM,OAAO,IAAI,OAAO,MAAM;AACtC,QAAI,CAAC,OAAO,UAAU,IAAI,KAAK,QAAQ,GAAG;AACxC,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACA,QAAI,WAAW,QAAW;AACxB,0BAAoB,QAAQ,oBAAoB,OAAO;AAAA,IACzD;AAAA,EACF;AAEA,SAAO,OAAO;AAChB;","names":[]}
@@ -0,0 +1,6 @@
1
+ export { defineBetterConfig } from "./define-config.js";
2
+ export { loadBetterConfig, BetterConfigNotFoundError, } from "./config-loader.js";
3
+ export type { LoadBetterConfigOptions } from "./config-loader.js";
4
+ export { requireTenantConfig, requireAuditConfig, ConfigValidationError, } from "./config-validation.js";
5
+ export type { BetterConfig, BetterTenantStaticConfig, BetterAuditStaticConfig, BetterConsoleStaticConfig, TelemetryConfig, PathResolverConfig, SubdomainResolverConfig, StaticJwtResolverConfig, StaticTenantResolverConfig, RetentionPolicy, StaticMagicLinkConfig, StaticConsoleSessionConfig, } from "./config-types.js";
6
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EACL,gBAAgB,EAChB,yBAAyB,GAC1B,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAElE,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAIhC,YAAY,EACV,YAAY,EACZ,wBAAwB,EACxB,uBAAuB,EACvB,yBAAyB,EACzB,eAAe,EACf,kBAAkB,EAClB,uBAAuB,EACvB,uBAAuB,EACvB,0BAA0B,EAC1B,eAAe,EACf,qBAAqB,EACrB,0BAA0B,GAC3B,MAAM,mBAAmB,CAAC"}
package/dist/config.js ADDED
@@ -0,0 +1,110 @@
1
+ // src/define-config.ts
2
+ function defineBetterConfig(config) {
3
+ return config;
4
+ }
5
+
6
+ // src/config-loader.ts
7
+ import { loadConfig } from "c12";
8
+ var BetterConfigNotFoundError = class extends Error {
9
+ name = "BetterConfigNotFoundError";
10
+ cwd;
11
+ constructor(cwd) {
12
+ super(
13
+ [
14
+ `No better.config.{ts,js,mjs} file found in ${cwd}.`,
15
+ "",
16
+ "Create one with:",
17
+ "",
18
+ ' import { defineBetterConfig } from "@usebetterdev/plugin/config";',
19
+ "",
20
+ " export default defineBetterConfig({",
21
+ ' tenant: { tenantTables: ["organizations"] },',
22
+ " });"
23
+ ].join("\n")
24
+ );
25
+ this.cwd = cwd;
26
+ }
27
+ };
28
+ async function loadBetterConfig(options) {
29
+ const cwd = options?.cwd ?? process.cwd();
30
+ const configFile = options?.configPath;
31
+ const result = await loadConfig({
32
+ name: "better",
33
+ cwd,
34
+ ...configFile !== void 0 ? { configFile } : {},
35
+ rcFile: false,
36
+ globalRc: false,
37
+ packageJson: false,
38
+ dotenv: false
39
+ });
40
+ const layers = result.layers ?? [];
41
+ if (layers.length === 0) {
42
+ throw new BetterConfigNotFoundError(cwd);
43
+ }
44
+ return result.config ?? {};
45
+ }
46
+
47
+ // src/config-validation.ts
48
+ var ConfigValidationError = class extends Error {
49
+ name = "ConfigValidationError";
50
+ field;
51
+ constructor(field, message) {
52
+ super(`Config error: "${field}" ${message}`);
53
+ this.field = field;
54
+ }
55
+ };
56
+ function isNonEmptyString(value) {
57
+ return typeof value === "string" && value.length > 0;
58
+ }
59
+ function validateStringArray(items, fieldName, sectionName) {
60
+ const qualifiedName = `${sectionName}.${fieldName}`;
61
+ if (!Array.isArray(items)) {
62
+ throw new ConfigValidationError(qualifiedName, "must be an array");
63
+ }
64
+ if (items.length === 0) {
65
+ throw new ConfigValidationError(qualifiedName, "must not be empty");
66
+ }
67
+ for (const item of items) {
68
+ if (!isNonEmptyString(item)) {
69
+ throw new ConfigValidationError(
70
+ qualifiedName,
71
+ "entries must be non-empty strings"
72
+ );
73
+ }
74
+ }
75
+ }
76
+ function requireTenantConfig(config) {
77
+ if (config.tenant === void 0) {
78
+ throw new ConfigValidationError("tenant", "section is required");
79
+ }
80
+ validateStringArray(config.tenant.tenantTables, "tenantTables", "tenant");
81
+ return config.tenant;
82
+ }
83
+ function requireAuditConfig(config) {
84
+ if (config.audit === void 0) {
85
+ throw new ConfigValidationError("audit", "section is required");
86
+ }
87
+ validateStringArray(config.audit.auditTables, "auditTables", "audit");
88
+ if (config.audit.retention !== void 0) {
89
+ const { days, tables } = config.audit.retention;
90
+ if (!Number.isInteger(days) || days <= 0) {
91
+ throw new ConfigValidationError(
92
+ "audit.retention.days",
93
+ "must be a positive integer"
94
+ );
95
+ }
96
+ if (tables !== void 0) {
97
+ validateStringArray(tables, "retention.tables", "audit");
98
+ }
99
+ }
100
+ return config.audit;
101
+ }
102
+ export {
103
+ BetterConfigNotFoundError,
104
+ ConfigValidationError,
105
+ defineBetterConfig,
106
+ loadBetterConfig,
107
+ requireAuditConfig,
108
+ requireTenantConfig
109
+ };
110
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/define-config.ts","../src/config-loader.ts","../src/config-validation.ts"],"sourcesContent":["import type { BetterConfig } from \"./config-types.js\";\n\n/**\n * Identity helper that provides TypeScript autocomplete for `better.config.ts`.\n *\n * ```ts\n * // better.config.ts\n * import { defineBetterConfig } from \"@usebetterdev/plugin/config\";\n *\n * export default defineBetterConfig({\n * tenant: { tenantTables: [\"organizations\"] },\n * });\n * ```\n */\nexport function defineBetterConfig(config: BetterConfig): BetterConfig {\n return config;\n}\n","import { loadConfig } from \"c12\";\nimport type { BetterConfig } from \"./config-types.js\";\n\n/** Thrown when no `better.config.*` file is found in the working directory. */\nexport class BetterConfigNotFoundError extends Error {\n override readonly name = \"BetterConfigNotFoundError\";\n readonly cwd: string;\n\n constructor(cwd: string) {\n super(\n [\n `No better.config.{ts,js,mjs} file found in ${cwd}.`,\n \"\",\n \"Create one with:\",\n \"\",\n ' import { defineBetterConfig } from \"@usebetterdev/plugin/config\";',\n \"\",\n \" export default defineBetterConfig({\",\n ' tenant: { tenantTables: [\"organizations\"] },',\n \" });\",\n ].join(\"\\n\"),\n );\n this.cwd = cwd;\n }\n}\n\nexport interface LoadBetterConfigOptions {\n /** Working directory to search for the config file. Defaults to `process.cwd()`. */\n cwd?: string;\n /** Explicit path to a config file, bypassing discovery. Maps to c12's `configFile` option. */\n configPath?: string;\n}\n\n/**\n * Loads a `better.config.{ts,js,mjs}` file using c12.\n * Throws {@link BetterConfigNotFoundError} when no config file is found.\n *\n * The returned config is **unvalidated**. Pass it through\n * {@link requireTenantConfig} or {@link requireAuditConfig} to validate\n * product-specific sections.\n */\nexport async function loadBetterConfig(\n options?: LoadBetterConfigOptions,\n): Promise<BetterConfig> {\n const cwd = options?.cwd ?? process.cwd();\n\n // configPath maps to c12's configFile — spread conditionally to satisfy exactOptionalPropertyTypes\n const configFile = options?.configPath;\n\n const result = await loadConfig<BetterConfig>({\n name: \"better\",\n cwd,\n ...(configFile !== undefined ? { configFile } : {}),\n rcFile: false,\n globalRc: false,\n packageJson: false,\n dotenv: false,\n });\n\n const layers = result.layers ?? [];\n if (layers.length === 0) {\n throw new BetterConfigNotFoundError(cwd);\n }\n\n return result.config ?? {};\n}\n","import type {\n BetterConfig,\n BetterTenantStaticConfig,\n BetterAuditStaticConfig,\n} from \"./config-types.js\";\n\n/** Thrown when a config section fails validation. */\nexport class ConfigValidationError extends Error {\n override readonly name = \"ConfigValidationError\";\n readonly field: string;\n\n constructor(field: string, message: string) {\n super(`Config error: \"${field}\" ${message}`);\n this.field = field;\n }\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n\nfunction validateStringArray(\n items: unknown,\n fieldName: string,\n sectionName: string,\n): void {\n const qualifiedName = `${sectionName}.${fieldName}`;\n if (!Array.isArray(items)) {\n throw new ConfigValidationError(qualifiedName, \"must be an array\");\n }\n if (items.length === 0) {\n throw new ConfigValidationError(qualifiedName, \"must not be empty\");\n }\n for (const item of items) {\n if (!isNonEmptyString(item)) {\n throw new ConfigValidationError(\n qualifiedName,\n \"entries must be non-empty strings\",\n );\n }\n }\n}\n\n/**\n * Validates that `config.tenant` exists and its `tenantTables` is a non-empty\n * array of non-empty strings. Returns the narrowed tenant config.\n */\nexport function requireTenantConfig(\n config: BetterConfig,\n): BetterTenantStaticConfig {\n if (config.tenant === undefined) {\n throw new ConfigValidationError(\"tenant\", \"section is required\");\n }\n validateStringArray(config.tenant.tenantTables, \"tenantTables\", \"tenant\");\n return config.tenant;\n}\n\n/**\n * Validates that `config.audit` exists and its `auditTables` is a non-empty\n * array of non-empty strings. When `retention` is present, validates `days` is\n * a positive integer and `tables` (if present) is a non-empty string array.\n */\nexport function requireAuditConfig(\n config: BetterConfig,\n): BetterAuditStaticConfig {\n if (config.audit === undefined) {\n throw new ConfigValidationError(\"audit\", \"section is required\");\n }\n validateStringArray(config.audit.auditTables, \"auditTables\", \"audit\");\n\n if (config.audit.retention !== undefined) {\n const { days, tables } = config.audit.retention;\n if (!Number.isInteger(days) || days <= 0) {\n throw new ConfigValidationError(\n \"audit.retention.days\",\n \"must be a positive integer\",\n );\n }\n if (tables !== undefined) {\n validateStringArray(tables, \"retention.tables\", \"audit\");\n }\n }\n\n return config.audit;\n}\n"],"mappings":";AAcO,SAAS,mBAAmB,QAAoC;AACrE,SAAO;AACT;;;AChBA,SAAS,kBAAkB;AAIpB,IAAM,4BAAN,cAAwC,MAAM;AAAA,EACjC,OAAO;AAAA,EAChB;AAAA,EAET,YAAY,KAAa;AACvB;AAAA,MACE;AAAA,QACE,8CAA8C,GAAG;AAAA,QACjD;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,IAAI;AAAA,IACb;AACA,SAAK,MAAM;AAAA,EACb;AACF;AAiBA,eAAsB,iBACpB,SACuB;AACvB,QAAM,MAAM,SAAS,OAAO,QAAQ,IAAI;AAGxC,QAAM,aAAa,SAAS;AAE5B,QAAM,SAAS,MAAM,WAAyB;AAAA,IAC5C,MAAM;AAAA,IACN;AAAA,IACA,GAAI,eAAe,SAAY,EAAE,WAAW,IAAI,CAAC;AAAA,IACjD,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,aAAa;AAAA,IACb,QAAQ;AAAA,EACV,CAAC;AAED,QAAM,SAAS,OAAO,UAAU,CAAC;AACjC,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,0BAA0B,GAAG;AAAA,EACzC;AAEA,SAAO,OAAO,UAAU,CAAC;AAC3B;;;AC1DO,IAAM,wBAAN,cAAoC,MAAM;AAAA,EAC7B,OAAO;AAAA,EAChB;AAAA,EAET,YAAY,OAAe,SAAiB;AAC1C,UAAM,kBAAkB,KAAK,KAAK,OAAO,EAAE;AAC3C,SAAK,QAAQ;AAAA,EACf;AACF;AAEA,SAAS,iBAAiB,OAAiC;AACzD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS;AACrD;AAEA,SAAS,oBACP,OACA,WACA,aACM;AACN,QAAM,gBAAgB,GAAG,WAAW,IAAI,SAAS;AACjD,MAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,UAAM,IAAI,sBAAsB,eAAe,kBAAkB;AAAA,EACnE;AACA,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,IAAI,sBAAsB,eAAe,mBAAmB;AAAA,EACpE;AACA,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,iBAAiB,IAAI,GAAG;AAC3B,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAMO,SAAS,oBACd,QAC0B;AAC1B,MAAI,OAAO,WAAW,QAAW;AAC/B,UAAM,IAAI,sBAAsB,UAAU,qBAAqB;AAAA,EACjE;AACA,sBAAoB,OAAO,OAAO,cAAc,gBAAgB,QAAQ;AACxE,SAAO,OAAO;AAChB;AAOO,SAAS,mBACd,QACyB;AACzB,MAAI,OAAO,UAAU,QAAW;AAC9B,UAAM,IAAI,sBAAsB,SAAS,qBAAqB;AAAA,EAChE;AACA,sBAAoB,OAAO,MAAM,aAAa,eAAe,OAAO;AAEpE,MAAI,OAAO,MAAM,cAAc,QAAW;AACxC,UAAM,EAAE,MAAM,OAAO,IAAI,OAAO,MAAM;AACtC,QAAI,CAAC,OAAO,UAAU,IAAI,KAAK,QAAQ,GAAG;AACxC,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACA,QAAI,WAAW,QAAW;AACxB,0BAAoB,QAAQ,oBAAoB,OAAO;AAAA,IACzD;AAAA,EACF;AAEA,SAAO,OAAO;AAChB;","names":[]}
@@ -0,0 +1,15 @@
1
+ import type { BetterConfig } from "./config-types.js";
2
+ /**
3
+ * Identity helper that provides TypeScript autocomplete for `better.config.ts`.
4
+ *
5
+ * ```ts
6
+ * // better.config.ts
7
+ * import { defineBetterConfig } from "@usebetterdev/plugin/config";
8
+ *
9
+ * export default defineBetterConfig({
10
+ * tenant: { tenantTables: ["organizations"] },
11
+ * });
12
+ * ```
13
+ */
14
+ export declare function defineBetterConfig(config: BetterConfig): BetterConfig;
15
+ //# sourceMappingURL=define-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-config.d.ts","sourceRoot":"","sources":["../src/define-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAErE"}
@@ -0,0 +1,34 @@
1
+ export declare const authEvents: () => {
2
+ id: string;
3
+ schema: {
4
+ tables: {
5
+ auth_events: {
6
+ fields: {
7
+ id: {
8
+ type: "string";
9
+ required: true;
10
+ unique: true;
11
+ };
12
+ event_type: {
13
+ type: "string";
14
+ required: true;
15
+ };
16
+ user_id: {
17
+ type: "string";
18
+ required: true;
19
+ index: true;
20
+ };
21
+ timestamp: {
22
+ type: "date";
23
+ required: true;
24
+ defaultValue: () => Date;
25
+ };
26
+ metadata: {
27
+ type: "json";
28
+ };
29
+ };
30
+ };
31
+ };
32
+ };
33
+ };
34
+ //# sourceMappingURL=auth-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-events.d.ts","sourceRoot":"","sources":["../../src/demo/auth-events.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsBtB,CAAC"}
@@ -0,0 +1,74 @@
1
+ export type AuditHookContract = {
2
+ afterLog: {
3
+ log: Readonly<{
4
+ id: string;
5
+ tableName: string;
6
+ }>;
7
+ };
8
+ };
9
+ export interface WebhookDispatcherConfig {
10
+ /** Target URL to POST event payloads to. */
11
+ url: string;
12
+ /** Optional list of event types to forward. When omitted, all events are forwarded. */
13
+ events?: string[];
14
+ }
15
+ export declare const webhookDispatcher: (config: WebhookDispatcherConfig) => {
16
+ id: string;
17
+ hooks: {
18
+ afterLog: (_payload: {
19
+ log: Readonly<{
20
+ id: string;
21
+ tableName: string;
22
+ }>;
23
+ }) => Promise<void>;
24
+ };
25
+ schema: {
26
+ tables: {
27
+ webhook_deliveries: {
28
+ fields: {
29
+ id: {
30
+ type: "string";
31
+ required: true;
32
+ unique: true;
33
+ };
34
+ endpoint_url: {
35
+ type: "string";
36
+ required: true;
37
+ index: true;
38
+ };
39
+ event_type: {
40
+ type: "string";
41
+ required: true;
42
+ };
43
+ status: {
44
+ type: "string";
45
+ required: true;
46
+ };
47
+ http_status: {
48
+ type: "number";
49
+ };
50
+ attempt_count: {
51
+ type: "number";
52
+ required: true;
53
+ defaultValue: number;
54
+ };
55
+ last_attempt_at: {
56
+ type: "date";
57
+ };
58
+ created_at: {
59
+ type: "date";
60
+ required: true;
61
+ };
62
+ };
63
+ };
64
+ };
65
+ extend: {
66
+ audit_log: {
67
+ webhook_delivered_at: {
68
+ type: "date";
69
+ };
70
+ };
71
+ };
72
+ };
73
+ };
74
+ //# sourceMappingURL=webhook-dispatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-dispatcher.d.ts","sourceRoot":"","sources":["../../src/demo/webhook-dispatcher.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE;QAAE,GAAG,EAAE,QAAQ,CAAC;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC;CAChE,CAAC;AAEF,MAAM,WAAW,uBAAuB;IACtC,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,uFAAuF;IACvF,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,eAAO,MAAM,iBAAiB,WAAY,uBAAuB;;;;iBAV9C,QAAQ,CAAC;gBAAE,EAAE,EAAE,MAAM,CAAC;gBAAC,SAAS,EAAE,MAAM,CAAA;aAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6C7D,CAAC"}
package/dist/index.cjs CHANGED
@@ -152,8 +152,51 @@ function assertOneOf(value, allowed, name) {
152
152
  }
153
153
 
154
154
  // src/schema.ts
155
- function mergeSchemas(_plugins) {
156
- return { tables: {}, extend: {} };
155
+ function mergeSchemas(plugins) {
156
+ const tables = {};
157
+ const extend = {};
158
+ const tableOwners = /* @__PURE__ */ new Map();
159
+ const fieldOwners = /* @__PURE__ */ new Map();
160
+ for (const plugin of plugins) {
161
+ if (!plugin.schema) {
162
+ continue;
163
+ }
164
+ if (plugin.schema.tables) {
165
+ for (const [tableName, tableDef] of Object.entries(
166
+ plugin.schema.tables
167
+ )) {
168
+ const existingOwner = tableOwners.get(tableName);
169
+ if (existingOwner !== void 0) {
170
+ throw new Error(
171
+ `Schema conflict: table "${tableName}" defined by both "${existingOwner}" and "${plugin.id}"`
172
+ );
173
+ }
174
+ tableOwners.set(tableName, plugin.id);
175
+ tables[tableName] = tableDef;
176
+ }
177
+ }
178
+ if (plugin.schema.extend) {
179
+ for (const [tableName, fields] of Object.entries(plugin.schema.extend)) {
180
+ let tableExtensions = extend[tableName];
181
+ if (tableExtensions === void 0) {
182
+ tableExtensions = {};
183
+ extend[tableName] = tableExtensions;
184
+ }
185
+ for (const [fieldName, fieldDef] of Object.entries(fields)) {
186
+ const key = `${tableName}.${fieldName}`;
187
+ const existingOwner = fieldOwners.get(key);
188
+ if (existingOwner !== void 0) {
189
+ throw new Error(
190
+ `Schema conflict: field "${tableName}.${fieldName}" extended by both "${existingOwner}" and "${plugin.id}"`
191
+ );
192
+ }
193
+ fieldOwners.set(key, plugin.id);
194
+ tableExtensions[fieldName] = fieldDef;
195
+ }
196
+ }
197
+ }
198
+ }
199
+ return { tables, extend };
157
200
  }
158
201
 
159
202
  // src/resolve-plugins.ts
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/hook-registry.ts","../src/validation.ts","../src/schema.ts","../src/resolve-plugins.ts"],"sourcesContent":["export type {\n HookContract,\n HookHandlers,\n PluginContext,\n FieldType,\n PluginFieldDefinition,\n PluginTableDefinition,\n PluginSchema,\n HttpMethod,\n PluginRequestContext,\n PluginResponse,\n PluginEndpointHandler,\n PluginEndpoint,\n PluginDefinition,\n} from \"./types.js\";\n\nexport { HookRegistry, PluginInvokeError } from \"./hook-registry.js\";\nexport type { PluginError } from \"./hook-registry.js\";\n\nexport { assertDefined, assertOneOf } from \"./validation.js\";\n\nexport { mergeSchemas } from \"./schema.js\";\nexport type { MergedPluginSchema } from \"./schema.js\";\n\nexport { resolvePlugins, PluginInitError } from \"./resolve-plugins.js\";\nexport type { ResolvedPlugins, ResolvedEndpoint } from \"./resolve-plugins.js\";\n","import type { HookContract, HookHandlers } from \"./types.js\";\n\n/**\n * Error captured when a plugin hook handler fails during `invokeAfter`.\n */\nexport interface PluginError {\n pluginId: string;\n hook: string;\n error: unknown;\n}\n\n/**\n * Error thrown by `invokeBefore` when a plugin hook handler fails.\n * Wraps the original error with plugin and hook context so callers\n * can identify which plugin caused the failure.\n */\nexport class PluginInvokeError extends Error {\n override readonly name = \"PluginInvokeError\";\n readonly pluginId: string;\n readonly hook: string;\n\n constructor(pluginId: string, hook: string, cause: unknown) {\n const detail = cause instanceof Error ? cause.message : String(cause);\n super(`Plugin \"${pluginId}\" failed on hook \"${hook}\": ${detail}`, {\n cause,\n });\n this.pluginId = pluginId;\n this.hook = hook;\n }\n}\n\n/**\n * Internal handler entry. The handler is typed as `(payload: never) => …`\n * because any function `(payload: T) => R` is assignable to it (contravariance\n * on `never`). This avoids a type assertion at registration time.\n */\ninterface RegisteredHook {\n pluginId: string;\n handler: (payload: never) => void | Promise<void>;\n}\n\n/**\n * Runtime registry that products use to register plugin hooks and invoke them\n * at lifecycle points.\n *\n * Generic over THooks so products get type-safe hook names and payloads.\n */\nexport class HookRegistry<THooks extends HookContract> {\n private hooks = new Map<string, RegisteredHook[]>();\n private registeredPlugins = new Set<string>();\n\n /**\n * Register all hooks declared by a plugin. Skips entries whose value is not\n * a function (defensive against partial/optional declarations).\n *\n * Throws if a plugin with the same ID has already been registered.\n */\n register(pluginId: string, hooks: Partial<HookHandlers<THooks>>): void {\n if (this.registeredPlugins.has(pluginId)) {\n throw new Error(`Plugin \"${pluginId}\" is already registered`);\n }\n this.registeredPlugins.add(pluginId);\n\n for (const [key, handler] of Object.entries(hooks)) {\n if (typeof handler !== \"function\") {\n continue;\n }\n\n let list = this.hooks.get(key);\n if (list === undefined) {\n list = [];\n this.hooks.set(key, list);\n }\n list.push({ pluginId, handler });\n }\n }\n\n /**\n * Sequential, fail-fast invocation — for \"before\" hooks.\n * Runs handlers in registration order. Aborts on first throw and wraps\n * the error in a {@link PluginInvokeError} so the caller knows which\n * plugin failed. Later handlers are **not** called.\n *\n * Uses a snapshot of the handler list so that registrations during\n * async execution do not affect the current invocation.\n */\n async invokeBefore<K extends keyof THooks & string>(\n hook: K,\n payload: THooks[K],\n ): Promise<void> {\n const list = this.hooks.get(hook);\n if (list === undefined) {\n return;\n }\n const snapshot = [...list];\n for (const entry of snapshot) {\n try {\n await this.callHandler(entry.handler, payload);\n } catch (error: unknown) {\n throw new PluginInvokeError(entry.pluginId, hook, error);\n }\n }\n }\n\n /**\n * Sequential, collect-errors invocation — for \"after\" hooks.\n * Runs **all** handlers even when some fail. Failures are wrapped in\n * {@link PluginError} and returned as an array. Returns an empty array\n * when all handlers succeed or when no handlers are registered.\n *\n * Uses a snapshot of the handler list so that registrations during\n * async execution do not affect the current invocation.\n */\n async invokeAfter<K extends keyof THooks & string>(\n hook: K,\n payload: THooks[K],\n ): Promise<PluginError[]> {\n const list = this.hooks.get(hook);\n if (list === undefined) {\n return [];\n }\n\n const errors: PluginError[] = [];\n const snapshot = [...list];\n for (const entry of snapshot) {\n try {\n await this.callHandler(entry.handler, payload);\n } catch (error: unknown) {\n errors.push({\n pluginId: entry.pluginId,\n hook,\n error,\n });\n }\n }\n return errors;\n }\n\n /**\n * Calls a stored handler with the given payload.\n *\n * Handlers are stored as `(payload: never) => …` for variance compatibility\n * (see {@link RegisteredHook}). At runtime the payload always matches\n * `THooks[K]` — guaranteed by the generic constraint on {@link register}.\n * This single assertion is the only place where TypeScript's type system\n * cannot statically verify the heterogeneous map access.\n */\n private callHandler<K extends keyof THooks>(\n handler: (payload: never) => void | Promise<void>,\n payload: THooks[K],\n ): void | Promise<void> {\n return (handler as (p: THooks[K]) => void | Promise<void>)(payload);\n }\n}\n","/**\n * Asserts that a value is not `undefined`.\n * Throws with a descriptive error message following the plugin config error pattern.\n */\nexport function assertDefined<T>(\n value: T | undefined,\n name: string,\n): asserts value is T {\n if (value === undefined) {\n throw new Error(`Plugin config error: \"${name}\" is required`);\n }\n}\n\n/**\n * Asserts that a value is one of the allowed values.\n * Throws with a descriptive error message listing the allowed options.\n */\nexport function assertOneOf<T extends string | number | boolean>(value: T, allowed: T[], name: string): void {\n if (!allowed.includes(value)) {\n const allowedStr = allowed.join(\", \");\n throw new Error(\n `Plugin config error: \"${name}\" must be one of: ${allowedStr} (got: ${String(value)})`,\n );\n }\n}\n","import type {\n HookContract,\n PluginDefinition,\n PluginFieldDefinition,\n PluginTableDefinition,\n} from \"./types.js\";\n\n/**\n * Merged schema produced by combining all plugin schema declarations.\n * Unlike per-plugin `PluginSchema` (where fields are optional),\n * the merged result always contains both maps.\n */\nexport interface MergedPluginSchema {\n tables: Record<string, PluginTableDefinition>;\n extend: Record<string, Record<string, PluginFieldDefinition>>;\n}\n\n/**\n * Merges schema declarations from all plugins into a single result.\n *\n * Returns empty maps. Full merge logic (conflict detection, ordering)\n * will be added when schema merging is needed.\n */\nexport function mergeSchemas<THooks extends HookContract>(\n _plugins: PluginDefinition<THooks>[],\n): MergedPluginSchema {\n return { tables: {}, extend: {} };\n}\n","import { HookRegistry } from \"./hook-registry.js\";\nimport type { MergedPluginSchema } from \"./schema.js\";\nimport { mergeSchemas } from \"./schema.js\";\nimport type {\n HookContract,\n PluginContext,\n PluginDefinition,\n PluginEndpoint,\n} from \"./types.js\";\n\nexport class PluginInitError extends Error {\n override readonly name = \"PluginInitError\";\n readonly pluginId: string;\n\n constructor(pluginId: string, cause: unknown) {\n const detail = cause instanceof Error ? cause.message : String(cause);\n super(`Plugin \"${pluginId}\" failed during init: ${detail}`, { cause });\n this.pluginId = pluginId;\n }\n}\n\n/**\n * A collected endpoint annotated with the plugin that contributed it.\n */\nexport interface ResolvedEndpoint extends PluginEndpoint {\n pluginId: string;\n}\n\n/**\n * Result of resolving a set of plugins. Contains the fully wired\n * hook registry, collected endpoints, merged schema, and plugin IDs.\n */\nexport interface ResolvedPlugins<THooks extends HookContract> {\n hookRegistry: HookRegistry<THooks>;\n endpoints: ResolvedEndpoint[];\n schema: MergedPluginSchema;\n pluginIds: string[];\n}\n\n/**\n * Lifecycle orchestrator that products call during construction.\n *\n * Steps:\n * 1. **Validate** — duplicate IDs throw immediately\n * 2. **Register hooks** — into a new HookRegistry\n * 3. **Collect endpoints** — flat array from all plugins\n * 4. **Merge schemas** — calls mergeSchemas\n * 5. **Init** — call plugin.init(ctx) sequentially in declaration order\n */\nexport async function resolvePlugins<THooks extends HookContract>(\n productId: string,\n plugins: PluginDefinition<THooks>[],\n config: Record<string, unknown>,\n): Promise<ResolvedPlugins<THooks>> {\n // 1. Validate — check for empty/whitespace IDs and duplicates\n const seen = new Set<string>();\n for (const plugin of plugins) {\n if (!plugin.id || !plugin.id.trim()) {\n throw new Error('Plugin config error: \"id\" is required');\n }\n if (plugin.id !== plugin.id.trim()) {\n throw new Error(\n `Plugin config error: \"id\" must not have leading or trailing whitespace (got: \"${plugin.id}\")`,\n );\n }\n if (seen.has(plugin.id)) {\n throw new Error(`Duplicate plugin ID: \"${plugin.id}\"`);\n }\n seen.add(plugin.id);\n }\n\n // 2. Register hooks\n const hookRegistry = new HookRegistry<THooks>();\n for (const plugin of plugins) {\n if (plugin.hooks) {\n hookRegistry.register(plugin.id, plugin.hooks);\n }\n }\n\n // 3. Collect endpoints (annotated with pluginId)\n const endpoints: ResolvedEndpoint[] = [];\n for (const plugin of plugins) {\n if (plugin.endpoints) {\n for (const ep of plugin.endpoints) {\n if (!ep.path || !ep.path.startsWith(\"/\")) {\n throw new Error(\n `Plugin \"${plugin.id}\" endpoint path must start with \"/\" (got: \"${ep.path}\")`,\n );\n }\n endpoints.push({ ...ep, pluginId: plugin.id });\n }\n }\n }\n\n // 4. Merge schemas\n const schema = mergeSchemas(plugins);\n\n // 5. Init — sequential in declaration order\n const pluginIds = plugins.map((p) => p.id);\n const frozenConfig = Object.freeze({ ...config });\n const context: PluginContext = {\n productId,\n hasPlugin: (id: string) => seen.has(id),\n config: frozenConfig,\n };\n\n for (const plugin of plugins) {\n if (plugin.init) {\n try {\n await plugin.init(context);\n } catch (err) {\n throw new PluginInitError(plugin.id, err);\n }\n }\n }\n\n return { hookRegistry, endpoints, schema, pluginIds };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/hook-registry.ts","../src/validation.ts","../src/schema.ts","../src/resolve-plugins.ts"],"sourcesContent":["export type {\n HookContract,\n HookHandlers,\n PluginContext,\n FieldType,\n PluginFieldDefinition,\n PluginTableDefinition,\n PluginSchema,\n HttpMethod,\n PluginRequestContext,\n PluginResponse,\n PluginEndpointHandler,\n PluginEndpoint,\n PluginDefinition,\n} from \"./types.js\";\n\nexport { HookRegistry, PluginInvokeError } from \"./hook-registry.js\";\nexport type { PluginError } from \"./hook-registry.js\";\n\nexport { assertDefined, assertOneOf } from \"./validation.js\";\n\nexport { mergeSchemas } from \"./schema.js\";\nexport type { MergedPluginSchema } from \"./schema.js\";\n\nexport { resolvePlugins, PluginInitError } from \"./resolve-plugins.js\";\nexport type { ResolvedPlugins, ResolvedEndpoint } from \"./resolve-plugins.js\";\n\nexport type {\n BetterConfig,\n BetterTenantStaticConfig,\n BetterAuditStaticConfig,\n BetterConsoleStaticConfig,\n} from \"./config-types.js\";\n","import type { HookContract, HookHandlers } from \"./types.js\";\n\n/**\n * Error captured when a plugin hook handler fails during `invokeAfter`.\n */\nexport interface PluginError {\n pluginId: string;\n hook: string;\n error: unknown;\n}\n\n/**\n * Error thrown by `invokeBefore` when a plugin hook handler fails.\n * Wraps the original error with plugin and hook context so callers\n * can identify which plugin caused the failure.\n */\nexport class PluginInvokeError extends Error {\n override readonly name = \"PluginInvokeError\";\n readonly pluginId: string;\n readonly hook: string;\n\n constructor(pluginId: string, hook: string, cause: unknown) {\n const detail = cause instanceof Error ? cause.message : String(cause);\n super(`Plugin \"${pluginId}\" failed on hook \"${hook}\": ${detail}`, {\n cause,\n });\n this.pluginId = pluginId;\n this.hook = hook;\n }\n}\n\n/**\n * Internal handler entry. The handler is typed as `(payload: never) => …`\n * because any function `(payload: T) => R` is assignable to it (contravariance\n * on `never`). This avoids a type assertion at registration time.\n */\ninterface RegisteredHook {\n pluginId: string;\n handler: (payload: never) => void | Promise<void>;\n}\n\n/**\n * Runtime registry that products use to register plugin hooks and invoke them\n * at lifecycle points.\n *\n * Generic over THooks so products get type-safe hook names and payloads.\n */\nexport class HookRegistry<THooks extends HookContract> {\n private hooks = new Map<string, RegisteredHook[]>();\n private registeredPlugins = new Set<string>();\n\n /**\n * Register all hooks declared by a plugin. Skips entries whose value is not\n * a function (defensive against partial/optional declarations).\n *\n * Throws if a plugin with the same ID has already been registered.\n */\n register(pluginId: string, hooks: Partial<HookHandlers<THooks>>): void {\n if (this.registeredPlugins.has(pluginId)) {\n throw new Error(`Plugin \"${pluginId}\" is already registered`);\n }\n this.registeredPlugins.add(pluginId);\n\n for (const [key, handler] of Object.entries(hooks)) {\n if (typeof handler !== \"function\") {\n continue;\n }\n\n let list = this.hooks.get(key);\n if (list === undefined) {\n list = [];\n this.hooks.set(key, list);\n }\n list.push({ pluginId, handler });\n }\n }\n\n /**\n * Sequential, fail-fast invocation — for \"before\" hooks.\n * Runs handlers in registration order. Aborts on first throw and wraps\n * the error in a {@link PluginInvokeError} so the caller knows which\n * plugin failed. Later handlers are **not** called.\n *\n * Uses a snapshot of the handler list so that registrations during\n * async execution do not affect the current invocation.\n */\n async invokeBefore<K extends keyof THooks & string>(\n hook: K,\n payload: THooks[K],\n ): Promise<void> {\n const list = this.hooks.get(hook);\n if (list === undefined) {\n return;\n }\n const snapshot = [...list];\n for (const entry of snapshot) {\n try {\n await this.callHandler(entry.handler, payload);\n } catch (error: unknown) {\n throw new PluginInvokeError(entry.pluginId, hook, error);\n }\n }\n }\n\n /**\n * Sequential, collect-errors invocation — for \"after\" hooks.\n * Runs **all** handlers even when some fail. Failures are wrapped in\n * {@link PluginError} and returned as an array. Returns an empty array\n * when all handlers succeed or when no handlers are registered.\n *\n * Uses a snapshot of the handler list so that registrations during\n * async execution do not affect the current invocation.\n */\n async invokeAfter<K extends keyof THooks & string>(\n hook: K,\n payload: THooks[K],\n ): Promise<PluginError[]> {\n const list = this.hooks.get(hook);\n if (list === undefined) {\n return [];\n }\n\n const errors: PluginError[] = [];\n const snapshot = [...list];\n for (const entry of snapshot) {\n try {\n await this.callHandler(entry.handler, payload);\n } catch (error: unknown) {\n errors.push({\n pluginId: entry.pluginId,\n hook,\n error,\n });\n }\n }\n return errors;\n }\n\n /**\n * Calls a stored handler with the given payload.\n *\n * Handlers are stored as `(payload: never) => …` for variance compatibility\n * (see {@link RegisteredHook}). At runtime the payload always matches\n * `THooks[K]` — guaranteed by the generic constraint on {@link register}.\n * This single assertion is the only place where TypeScript's type system\n * cannot statically verify the heterogeneous map access.\n */\n private callHandler<K extends keyof THooks>(\n handler: (payload: never) => void | Promise<void>,\n payload: THooks[K],\n ): void | Promise<void> {\n return (handler as (p: THooks[K]) => void | Promise<void>)(payload);\n }\n}\n","/**\n * Asserts that a value is not `undefined`.\n * Throws with a descriptive error message following the plugin config error pattern.\n */\nexport function assertDefined<T>(\n value: T | undefined,\n name: string,\n): asserts value is T {\n if (value === undefined) {\n throw new Error(`Plugin config error: \"${name}\" is required`);\n }\n}\n\n/**\n * Asserts that a value is one of the allowed values.\n * Throws with a descriptive error message listing the allowed options.\n */\nexport function assertOneOf<T extends string | number | boolean>(value: T, allowed: T[], name: string): void {\n if (!allowed.includes(value)) {\n const allowedStr = allowed.join(\", \");\n throw new Error(\n `Plugin config error: \"${name}\" must be one of: ${allowedStr} (got: ${String(value)})`,\n );\n }\n}\n","import type {\n HookContract,\n PluginDefinition,\n PluginFieldDefinition,\n PluginTableDefinition,\n} from \"./types.js\";\n\n/**\n * Merged schema produced by combining all plugin schema declarations.\n * Unlike per-plugin `PluginSchema` (where fields are optional),\n * the merged result always contains both maps.\n */\nexport interface MergedPluginSchema {\n tables: Record<string, PluginTableDefinition>;\n extend: Record<string, Record<string, PluginFieldDefinition>>;\n}\n\n/**\n * Merges schema declarations from all plugins into a single result.\n *\n * Iterates plugins in declaration order and collects new tables and\n * extended fields. Throws on conflicts:\n * - Two plugins defining the same table name\n * - Two plugins extending the same field on the same table\n */\nexport function mergeSchemas<THooks extends HookContract>(\n plugins: PluginDefinition<THooks>[],\n): MergedPluginSchema {\n const tables: Record<string, PluginTableDefinition> = {};\n const extend: Record<string, Record<string, PluginFieldDefinition>> = {};\n\n const tableOwners = new Map<string, string>();\n const fieldOwners = new Map<string, string>();\n\n for (const plugin of plugins) {\n if (!plugin.schema) {\n continue;\n }\n\n if (plugin.schema.tables) {\n for (const [tableName, tableDef] of Object.entries(\n plugin.schema.tables,\n )) {\n const existingOwner = tableOwners.get(tableName);\n if (existingOwner !== undefined) {\n throw new Error(\n `Schema conflict: table \"${tableName}\" defined by both \"${existingOwner}\" and \"${plugin.id}\"`,\n );\n }\n tableOwners.set(tableName, plugin.id);\n tables[tableName] = tableDef;\n }\n }\n\n if (plugin.schema.extend) {\n for (const [tableName, fields] of Object.entries(plugin.schema.extend)) {\n let tableExtensions = extend[tableName];\n if (tableExtensions === undefined) {\n tableExtensions = {};\n extend[tableName] = tableExtensions;\n }\n\n for (const [fieldName, fieldDef] of Object.entries(fields)) {\n const key = `${tableName}.${fieldName}`;\n const existingOwner = fieldOwners.get(key);\n if (existingOwner !== undefined) {\n throw new Error(\n `Schema conflict: field \"${tableName}.${fieldName}\" extended by both \"${existingOwner}\" and \"${plugin.id}\"`,\n );\n }\n fieldOwners.set(key, plugin.id);\n tableExtensions[fieldName] = fieldDef;\n }\n }\n }\n }\n\n return { tables, extend };\n}\n","import { HookRegistry } from \"./hook-registry.js\";\nimport type { MergedPluginSchema } from \"./schema.js\";\nimport { mergeSchemas } from \"./schema.js\";\nimport type {\n HookContract,\n PluginContext,\n PluginDefinition,\n PluginEndpoint,\n} from \"./types.js\";\n\nexport class PluginInitError extends Error {\n override readonly name = \"PluginInitError\";\n readonly pluginId: string;\n\n constructor(pluginId: string, cause: unknown) {\n const detail = cause instanceof Error ? cause.message : String(cause);\n super(`Plugin \"${pluginId}\" failed during init: ${detail}`, { cause });\n this.pluginId = pluginId;\n }\n}\n\n/**\n * A collected endpoint annotated with the plugin that contributed it.\n */\nexport interface ResolvedEndpoint extends PluginEndpoint {\n pluginId: string;\n}\n\n/**\n * Result of resolving a set of plugins. Contains the fully wired\n * hook registry, collected endpoints, merged schema, and plugin IDs.\n */\nexport interface ResolvedPlugins<THooks extends HookContract> {\n hookRegistry: HookRegistry<THooks>;\n endpoints: ResolvedEndpoint[];\n schema: MergedPluginSchema;\n pluginIds: string[];\n}\n\n/**\n * Lifecycle orchestrator that products call during construction.\n *\n * Steps:\n * 1. **Validate** — duplicate IDs throw immediately\n * 2. **Register hooks** — into a new HookRegistry\n * 3. **Collect endpoints** — flat array from all plugins\n * 4. **Merge schemas** — calls mergeSchemas\n * 5. **Init** — call plugin.init(ctx) sequentially in declaration order\n */\nexport async function resolvePlugins<THooks extends HookContract>(\n productId: string,\n plugins: PluginDefinition<THooks>[],\n config: Record<string, unknown>,\n): Promise<ResolvedPlugins<THooks>> {\n // 1. Validate — check for empty/whitespace IDs and duplicates\n const seen = new Set<string>();\n for (const plugin of plugins) {\n if (!plugin.id || !plugin.id.trim()) {\n throw new Error('Plugin config error: \"id\" is required');\n }\n if (plugin.id !== plugin.id.trim()) {\n throw new Error(\n `Plugin config error: \"id\" must not have leading or trailing whitespace (got: \"${plugin.id}\")`,\n );\n }\n if (seen.has(plugin.id)) {\n throw new Error(`Duplicate plugin ID: \"${plugin.id}\"`);\n }\n seen.add(plugin.id);\n }\n\n // 2. Register hooks\n const hookRegistry = new HookRegistry<THooks>();\n for (const plugin of plugins) {\n if (plugin.hooks) {\n hookRegistry.register(plugin.id, plugin.hooks);\n }\n }\n\n // 3. Collect endpoints (annotated with pluginId)\n const endpoints: ResolvedEndpoint[] = [];\n for (const plugin of plugins) {\n if (plugin.endpoints) {\n for (const ep of plugin.endpoints) {\n if (!ep.path || !ep.path.startsWith(\"/\")) {\n throw new Error(\n `Plugin \"${plugin.id}\" endpoint path must start with \"/\" (got: \"${ep.path}\")`,\n );\n }\n endpoints.push({ ...ep, pluginId: plugin.id });\n }\n }\n }\n\n // 4. Merge schemas\n const schema = mergeSchemas(plugins);\n\n // 5. Init — sequential in declaration order\n const pluginIds = plugins.map((p) => p.id);\n const frozenConfig = Object.freeze({ ...config });\n const context: PluginContext = {\n productId,\n hasPlugin: (id: string) => seen.has(id),\n config: frozenConfig,\n };\n\n for (const plugin of plugins) {\n if (plugin.init) {\n try {\n await plugin.init(context);\n } catch (err) {\n throw new PluginInitError(plugin.id, err);\n }\n }\n }\n\n return { hookRegistry, endpoints, schema, pluginIds };\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -6,4 +6,5 @@ export { mergeSchemas } from "./schema.js";
6
6
  export type { MergedPluginSchema } from "./schema.js";
7
7
  export { resolvePlugins, PluginInitError } from "./resolve-plugins.js";
8
8
  export type { ResolvedPlugins, ResolvedEndpoint } from "./resolve-plugins.js";
9
+ export type { BetterConfig, BetterTenantStaticConfig, BetterAuditStaticConfig, BetterConsoleStaticConfig, } from "./config-types.js";
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,SAAS,EACT,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,EACZ,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,qBAAqB,EACrB,cAAc,EACd,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACrE,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvE,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,SAAS,EACT,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,EACZ,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,qBAAqB,EACrB,cAAc,EACd,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACrE,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvE,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAE9E,YAAY,EACV,YAAY,EACZ,wBAAwB,EACxB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -120,8 +120,51 @@ function assertOneOf(value, allowed, name) {
120
120
  }
121
121
 
122
122
  // src/schema.ts
123
- function mergeSchemas(_plugins) {
124
- return { tables: {}, extend: {} };
123
+ function mergeSchemas(plugins) {
124
+ const tables = {};
125
+ const extend = {};
126
+ const tableOwners = /* @__PURE__ */ new Map();
127
+ const fieldOwners = /* @__PURE__ */ new Map();
128
+ for (const plugin of plugins) {
129
+ if (!plugin.schema) {
130
+ continue;
131
+ }
132
+ if (plugin.schema.tables) {
133
+ for (const [tableName, tableDef] of Object.entries(
134
+ plugin.schema.tables
135
+ )) {
136
+ const existingOwner = tableOwners.get(tableName);
137
+ if (existingOwner !== void 0) {
138
+ throw new Error(
139
+ `Schema conflict: table "${tableName}" defined by both "${existingOwner}" and "${plugin.id}"`
140
+ );
141
+ }
142
+ tableOwners.set(tableName, plugin.id);
143
+ tables[tableName] = tableDef;
144
+ }
145
+ }
146
+ if (plugin.schema.extend) {
147
+ for (const [tableName, fields] of Object.entries(plugin.schema.extend)) {
148
+ let tableExtensions = extend[tableName];
149
+ if (tableExtensions === void 0) {
150
+ tableExtensions = {};
151
+ extend[tableName] = tableExtensions;
152
+ }
153
+ for (const [fieldName, fieldDef] of Object.entries(fields)) {
154
+ const key = `${tableName}.${fieldName}`;
155
+ const existingOwner = fieldOwners.get(key);
156
+ if (existingOwner !== void 0) {
157
+ throw new Error(
158
+ `Schema conflict: field "${tableName}.${fieldName}" extended by both "${existingOwner}" and "${plugin.id}"`
159
+ );
160
+ }
161
+ fieldOwners.set(key, plugin.id);
162
+ tableExtensions[fieldName] = fieldDef;
163
+ }
164
+ }
165
+ }
166
+ }
167
+ return { tables, extend };
125
168
  }
126
169
 
127
170
  // src/resolve-plugins.ts
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hook-registry.ts","../src/validation.ts","../src/schema.ts","../src/resolve-plugins.ts"],"sourcesContent":["import type { HookContract, HookHandlers } from \"./types.js\";\n\n/**\n * Error captured when a plugin hook handler fails during `invokeAfter`.\n */\nexport interface PluginError {\n pluginId: string;\n hook: string;\n error: unknown;\n}\n\n/**\n * Error thrown by `invokeBefore` when a plugin hook handler fails.\n * Wraps the original error with plugin and hook context so callers\n * can identify which plugin caused the failure.\n */\nexport class PluginInvokeError extends Error {\n override readonly name = \"PluginInvokeError\";\n readonly pluginId: string;\n readonly hook: string;\n\n constructor(pluginId: string, hook: string, cause: unknown) {\n const detail = cause instanceof Error ? cause.message : String(cause);\n super(`Plugin \"${pluginId}\" failed on hook \"${hook}\": ${detail}`, {\n cause,\n });\n this.pluginId = pluginId;\n this.hook = hook;\n }\n}\n\n/**\n * Internal handler entry. The handler is typed as `(payload: never) => …`\n * because any function `(payload: T) => R` is assignable to it (contravariance\n * on `never`). This avoids a type assertion at registration time.\n */\ninterface RegisteredHook {\n pluginId: string;\n handler: (payload: never) => void | Promise<void>;\n}\n\n/**\n * Runtime registry that products use to register plugin hooks and invoke them\n * at lifecycle points.\n *\n * Generic over THooks so products get type-safe hook names and payloads.\n */\nexport class HookRegistry<THooks extends HookContract> {\n private hooks = new Map<string, RegisteredHook[]>();\n private registeredPlugins = new Set<string>();\n\n /**\n * Register all hooks declared by a plugin. Skips entries whose value is not\n * a function (defensive against partial/optional declarations).\n *\n * Throws if a plugin with the same ID has already been registered.\n */\n register(pluginId: string, hooks: Partial<HookHandlers<THooks>>): void {\n if (this.registeredPlugins.has(pluginId)) {\n throw new Error(`Plugin \"${pluginId}\" is already registered`);\n }\n this.registeredPlugins.add(pluginId);\n\n for (const [key, handler] of Object.entries(hooks)) {\n if (typeof handler !== \"function\") {\n continue;\n }\n\n let list = this.hooks.get(key);\n if (list === undefined) {\n list = [];\n this.hooks.set(key, list);\n }\n list.push({ pluginId, handler });\n }\n }\n\n /**\n * Sequential, fail-fast invocation — for \"before\" hooks.\n * Runs handlers in registration order. Aborts on first throw and wraps\n * the error in a {@link PluginInvokeError} so the caller knows which\n * plugin failed. Later handlers are **not** called.\n *\n * Uses a snapshot of the handler list so that registrations during\n * async execution do not affect the current invocation.\n */\n async invokeBefore<K extends keyof THooks & string>(\n hook: K,\n payload: THooks[K],\n ): Promise<void> {\n const list = this.hooks.get(hook);\n if (list === undefined) {\n return;\n }\n const snapshot = [...list];\n for (const entry of snapshot) {\n try {\n await this.callHandler(entry.handler, payload);\n } catch (error: unknown) {\n throw new PluginInvokeError(entry.pluginId, hook, error);\n }\n }\n }\n\n /**\n * Sequential, collect-errors invocation — for \"after\" hooks.\n * Runs **all** handlers even when some fail. Failures are wrapped in\n * {@link PluginError} and returned as an array. Returns an empty array\n * when all handlers succeed or when no handlers are registered.\n *\n * Uses a snapshot of the handler list so that registrations during\n * async execution do not affect the current invocation.\n */\n async invokeAfter<K extends keyof THooks & string>(\n hook: K,\n payload: THooks[K],\n ): Promise<PluginError[]> {\n const list = this.hooks.get(hook);\n if (list === undefined) {\n return [];\n }\n\n const errors: PluginError[] = [];\n const snapshot = [...list];\n for (const entry of snapshot) {\n try {\n await this.callHandler(entry.handler, payload);\n } catch (error: unknown) {\n errors.push({\n pluginId: entry.pluginId,\n hook,\n error,\n });\n }\n }\n return errors;\n }\n\n /**\n * Calls a stored handler with the given payload.\n *\n * Handlers are stored as `(payload: never) => …` for variance compatibility\n * (see {@link RegisteredHook}). At runtime the payload always matches\n * `THooks[K]` — guaranteed by the generic constraint on {@link register}.\n * This single assertion is the only place where TypeScript's type system\n * cannot statically verify the heterogeneous map access.\n */\n private callHandler<K extends keyof THooks>(\n handler: (payload: never) => void | Promise<void>,\n payload: THooks[K],\n ): void | Promise<void> {\n return (handler as (p: THooks[K]) => void | Promise<void>)(payload);\n }\n}\n","/**\n * Asserts that a value is not `undefined`.\n * Throws with a descriptive error message following the plugin config error pattern.\n */\nexport function assertDefined<T>(\n value: T | undefined,\n name: string,\n): asserts value is T {\n if (value === undefined) {\n throw new Error(`Plugin config error: \"${name}\" is required`);\n }\n}\n\n/**\n * Asserts that a value is one of the allowed values.\n * Throws with a descriptive error message listing the allowed options.\n */\nexport function assertOneOf<T extends string | number | boolean>(value: T, allowed: T[], name: string): void {\n if (!allowed.includes(value)) {\n const allowedStr = allowed.join(\", \");\n throw new Error(\n `Plugin config error: \"${name}\" must be one of: ${allowedStr} (got: ${String(value)})`,\n );\n }\n}\n","import type {\n HookContract,\n PluginDefinition,\n PluginFieldDefinition,\n PluginTableDefinition,\n} from \"./types.js\";\n\n/**\n * Merged schema produced by combining all plugin schema declarations.\n * Unlike per-plugin `PluginSchema` (where fields are optional),\n * the merged result always contains both maps.\n */\nexport interface MergedPluginSchema {\n tables: Record<string, PluginTableDefinition>;\n extend: Record<string, Record<string, PluginFieldDefinition>>;\n}\n\n/**\n * Merges schema declarations from all plugins into a single result.\n *\n * Returns empty maps. Full merge logic (conflict detection, ordering)\n * will be added when schema merging is needed.\n */\nexport function mergeSchemas<THooks extends HookContract>(\n _plugins: PluginDefinition<THooks>[],\n): MergedPluginSchema {\n return { tables: {}, extend: {} };\n}\n","import { HookRegistry } from \"./hook-registry.js\";\nimport type { MergedPluginSchema } from \"./schema.js\";\nimport { mergeSchemas } from \"./schema.js\";\nimport type {\n HookContract,\n PluginContext,\n PluginDefinition,\n PluginEndpoint,\n} from \"./types.js\";\n\nexport class PluginInitError extends Error {\n override readonly name = \"PluginInitError\";\n readonly pluginId: string;\n\n constructor(pluginId: string, cause: unknown) {\n const detail = cause instanceof Error ? cause.message : String(cause);\n super(`Plugin \"${pluginId}\" failed during init: ${detail}`, { cause });\n this.pluginId = pluginId;\n }\n}\n\n/**\n * A collected endpoint annotated with the plugin that contributed it.\n */\nexport interface ResolvedEndpoint extends PluginEndpoint {\n pluginId: string;\n}\n\n/**\n * Result of resolving a set of plugins. Contains the fully wired\n * hook registry, collected endpoints, merged schema, and plugin IDs.\n */\nexport interface ResolvedPlugins<THooks extends HookContract> {\n hookRegistry: HookRegistry<THooks>;\n endpoints: ResolvedEndpoint[];\n schema: MergedPluginSchema;\n pluginIds: string[];\n}\n\n/**\n * Lifecycle orchestrator that products call during construction.\n *\n * Steps:\n * 1. **Validate** — duplicate IDs throw immediately\n * 2. **Register hooks** — into a new HookRegistry\n * 3. **Collect endpoints** — flat array from all plugins\n * 4. **Merge schemas** — calls mergeSchemas\n * 5. **Init** — call plugin.init(ctx) sequentially in declaration order\n */\nexport async function resolvePlugins<THooks extends HookContract>(\n productId: string,\n plugins: PluginDefinition<THooks>[],\n config: Record<string, unknown>,\n): Promise<ResolvedPlugins<THooks>> {\n // 1. Validate — check for empty/whitespace IDs and duplicates\n const seen = new Set<string>();\n for (const plugin of plugins) {\n if (!plugin.id || !plugin.id.trim()) {\n throw new Error('Plugin config error: \"id\" is required');\n }\n if (plugin.id !== plugin.id.trim()) {\n throw new Error(\n `Plugin config error: \"id\" must not have leading or trailing whitespace (got: \"${plugin.id}\")`,\n );\n }\n if (seen.has(plugin.id)) {\n throw new Error(`Duplicate plugin ID: \"${plugin.id}\"`);\n }\n seen.add(plugin.id);\n }\n\n // 2. Register hooks\n const hookRegistry = new HookRegistry<THooks>();\n for (const plugin of plugins) {\n if (plugin.hooks) {\n hookRegistry.register(plugin.id, plugin.hooks);\n }\n }\n\n // 3. Collect endpoints (annotated with pluginId)\n const endpoints: ResolvedEndpoint[] = [];\n for (const plugin of plugins) {\n if (plugin.endpoints) {\n for (const ep of plugin.endpoints) {\n if (!ep.path || !ep.path.startsWith(\"/\")) {\n throw new Error(\n `Plugin \"${plugin.id}\" endpoint path must start with \"/\" (got: \"${ep.path}\")`,\n );\n }\n endpoints.push({ ...ep, pluginId: plugin.id });\n }\n }\n }\n\n // 4. Merge schemas\n const schema = mergeSchemas(plugins);\n\n // 5. Init — sequential in declaration order\n const pluginIds = plugins.map((p) => p.id);\n const frozenConfig = Object.freeze({ ...config });\n const context: PluginContext = {\n productId,\n hasPlugin: (id: string) => seen.has(id),\n config: frozenConfig,\n };\n\n for (const plugin of plugins) {\n if (plugin.init) {\n try {\n await plugin.init(context);\n } catch (err) {\n throw new PluginInitError(plugin.id, err);\n }\n }\n }\n\n return { hookRegistry, endpoints, schema, pluginIds };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/hook-registry.ts","../src/validation.ts","../src/schema.ts","../src/resolve-plugins.ts"],"sourcesContent":["import type { HookContract, HookHandlers } from \"./types.js\";\n\n/**\n * Error captured when a plugin hook handler fails during `invokeAfter`.\n */\nexport interface PluginError {\n pluginId: string;\n hook: string;\n error: unknown;\n}\n\n/**\n * Error thrown by `invokeBefore` when a plugin hook handler fails.\n * Wraps the original error with plugin and hook context so callers\n * can identify which plugin caused the failure.\n */\nexport class PluginInvokeError extends Error {\n override readonly name = \"PluginInvokeError\";\n readonly pluginId: string;\n readonly hook: string;\n\n constructor(pluginId: string, hook: string, cause: unknown) {\n const detail = cause instanceof Error ? cause.message : String(cause);\n super(`Plugin \"${pluginId}\" failed on hook \"${hook}\": ${detail}`, {\n cause,\n });\n this.pluginId = pluginId;\n this.hook = hook;\n }\n}\n\n/**\n * Internal handler entry. The handler is typed as `(payload: never) => …`\n * because any function `(payload: T) => R` is assignable to it (contravariance\n * on `never`). This avoids a type assertion at registration time.\n */\ninterface RegisteredHook {\n pluginId: string;\n handler: (payload: never) => void | Promise<void>;\n}\n\n/**\n * Runtime registry that products use to register plugin hooks and invoke them\n * at lifecycle points.\n *\n * Generic over THooks so products get type-safe hook names and payloads.\n */\nexport class HookRegistry<THooks extends HookContract> {\n private hooks = new Map<string, RegisteredHook[]>();\n private registeredPlugins = new Set<string>();\n\n /**\n * Register all hooks declared by a plugin. Skips entries whose value is not\n * a function (defensive against partial/optional declarations).\n *\n * Throws if a plugin with the same ID has already been registered.\n */\n register(pluginId: string, hooks: Partial<HookHandlers<THooks>>): void {\n if (this.registeredPlugins.has(pluginId)) {\n throw new Error(`Plugin \"${pluginId}\" is already registered`);\n }\n this.registeredPlugins.add(pluginId);\n\n for (const [key, handler] of Object.entries(hooks)) {\n if (typeof handler !== \"function\") {\n continue;\n }\n\n let list = this.hooks.get(key);\n if (list === undefined) {\n list = [];\n this.hooks.set(key, list);\n }\n list.push({ pluginId, handler });\n }\n }\n\n /**\n * Sequential, fail-fast invocation — for \"before\" hooks.\n * Runs handlers in registration order. Aborts on first throw and wraps\n * the error in a {@link PluginInvokeError} so the caller knows which\n * plugin failed. Later handlers are **not** called.\n *\n * Uses a snapshot of the handler list so that registrations during\n * async execution do not affect the current invocation.\n */\n async invokeBefore<K extends keyof THooks & string>(\n hook: K,\n payload: THooks[K],\n ): Promise<void> {\n const list = this.hooks.get(hook);\n if (list === undefined) {\n return;\n }\n const snapshot = [...list];\n for (const entry of snapshot) {\n try {\n await this.callHandler(entry.handler, payload);\n } catch (error: unknown) {\n throw new PluginInvokeError(entry.pluginId, hook, error);\n }\n }\n }\n\n /**\n * Sequential, collect-errors invocation — for \"after\" hooks.\n * Runs **all** handlers even when some fail. Failures are wrapped in\n * {@link PluginError} and returned as an array. Returns an empty array\n * when all handlers succeed or when no handlers are registered.\n *\n * Uses a snapshot of the handler list so that registrations during\n * async execution do not affect the current invocation.\n */\n async invokeAfter<K extends keyof THooks & string>(\n hook: K,\n payload: THooks[K],\n ): Promise<PluginError[]> {\n const list = this.hooks.get(hook);\n if (list === undefined) {\n return [];\n }\n\n const errors: PluginError[] = [];\n const snapshot = [...list];\n for (const entry of snapshot) {\n try {\n await this.callHandler(entry.handler, payload);\n } catch (error: unknown) {\n errors.push({\n pluginId: entry.pluginId,\n hook,\n error,\n });\n }\n }\n return errors;\n }\n\n /**\n * Calls a stored handler with the given payload.\n *\n * Handlers are stored as `(payload: never) => …` for variance compatibility\n * (see {@link RegisteredHook}). At runtime the payload always matches\n * `THooks[K]` — guaranteed by the generic constraint on {@link register}.\n * This single assertion is the only place where TypeScript's type system\n * cannot statically verify the heterogeneous map access.\n */\n private callHandler<K extends keyof THooks>(\n handler: (payload: never) => void | Promise<void>,\n payload: THooks[K],\n ): void | Promise<void> {\n return (handler as (p: THooks[K]) => void | Promise<void>)(payload);\n }\n}\n","/**\n * Asserts that a value is not `undefined`.\n * Throws with a descriptive error message following the plugin config error pattern.\n */\nexport function assertDefined<T>(\n value: T | undefined,\n name: string,\n): asserts value is T {\n if (value === undefined) {\n throw new Error(`Plugin config error: \"${name}\" is required`);\n }\n}\n\n/**\n * Asserts that a value is one of the allowed values.\n * Throws with a descriptive error message listing the allowed options.\n */\nexport function assertOneOf<T extends string | number | boolean>(value: T, allowed: T[], name: string): void {\n if (!allowed.includes(value)) {\n const allowedStr = allowed.join(\", \");\n throw new Error(\n `Plugin config error: \"${name}\" must be one of: ${allowedStr} (got: ${String(value)})`,\n );\n }\n}\n","import type {\n HookContract,\n PluginDefinition,\n PluginFieldDefinition,\n PluginTableDefinition,\n} from \"./types.js\";\n\n/**\n * Merged schema produced by combining all plugin schema declarations.\n * Unlike per-plugin `PluginSchema` (where fields are optional),\n * the merged result always contains both maps.\n */\nexport interface MergedPluginSchema {\n tables: Record<string, PluginTableDefinition>;\n extend: Record<string, Record<string, PluginFieldDefinition>>;\n}\n\n/**\n * Merges schema declarations from all plugins into a single result.\n *\n * Iterates plugins in declaration order and collects new tables and\n * extended fields. Throws on conflicts:\n * - Two plugins defining the same table name\n * - Two plugins extending the same field on the same table\n */\nexport function mergeSchemas<THooks extends HookContract>(\n plugins: PluginDefinition<THooks>[],\n): MergedPluginSchema {\n const tables: Record<string, PluginTableDefinition> = {};\n const extend: Record<string, Record<string, PluginFieldDefinition>> = {};\n\n const tableOwners = new Map<string, string>();\n const fieldOwners = new Map<string, string>();\n\n for (const plugin of plugins) {\n if (!plugin.schema) {\n continue;\n }\n\n if (plugin.schema.tables) {\n for (const [tableName, tableDef] of Object.entries(\n plugin.schema.tables,\n )) {\n const existingOwner = tableOwners.get(tableName);\n if (existingOwner !== undefined) {\n throw new Error(\n `Schema conflict: table \"${tableName}\" defined by both \"${existingOwner}\" and \"${plugin.id}\"`,\n );\n }\n tableOwners.set(tableName, plugin.id);\n tables[tableName] = tableDef;\n }\n }\n\n if (plugin.schema.extend) {\n for (const [tableName, fields] of Object.entries(plugin.schema.extend)) {\n let tableExtensions = extend[tableName];\n if (tableExtensions === undefined) {\n tableExtensions = {};\n extend[tableName] = tableExtensions;\n }\n\n for (const [fieldName, fieldDef] of Object.entries(fields)) {\n const key = `${tableName}.${fieldName}`;\n const existingOwner = fieldOwners.get(key);\n if (existingOwner !== undefined) {\n throw new Error(\n `Schema conflict: field \"${tableName}.${fieldName}\" extended by both \"${existingOwner}\" and \"${plugin.id}\"`,\n );\n }\n fieldOwners.set(key, plugin.id);\n tableExtensions[fieldName] = fieldDef;\n }\n }\n }\n }\n\n return { tables, extend };\n}\n","import { HookRegistry } from \"./hook-registry.js\";\nimport type { MergedPluginSchema } from \"./schema.js\";\nimport { mergeSchemas } from \"./schema.js\";\nimport type {\n HookContract,\n PluginContext,\n PluginDefinition,\n PluginEndpoint,\n} from \"./types.js\";\n\nexport class PluginInitError extends Error {\n override readonly name = \"PluginInitError\";\n readonly pluginId: string;\n\n constructor(pluginId: string, cause: unknown) {\n const detail = cause instanceof Error ? cause.message : String(cause);\n super(`Plugin \"${pluginId}\" failed during init: ${detail}`, { cause });\n this.pluginId = pluginId;\n }\n}\n\n/**\n * A collected endpoint annotated with the plugin that contributed it.\n */\nexport interface ResolvedEndpoint extends PluginEndpoint {\n pluginId: string;\n}\n\n/**\n * Result of resolving a set of plugins. Contains the fully wired\n * hook registry, collected endpoints, merged schema, and plugin IDs.\n */\nexport interface ResolvedPlugins<THooks extends HookContract> {\n hookRegistry: HookRegistry<THooks>;\n endpoints: ResolvedEndpoint[];\n schema: MergedPluginSchema;\n pluginIds: string[];\n}\n\n/**\n * Lifecycle orchestrator that products call during construction.\n *\n * Steps:\n * 1. **Validate** — duplicate IDs throw immediately\n * 2. **Register hooks** — into a new HookRegistry\n * 3. **Collect endpoints** — flat array from all plugins\n * 4. **Merge schemas** — calls mergeSchemas\n * 5. **Init** — call plugin.init(ctx) sequentially in declaration order\n */\nexport async function resolvePlugins<THooks extends HookContract>(\n productId: string,\n plugins: PluginDefinition<THooks>[],\n config: Record<string, unknown>,\n): Promise<ResolvedPlugins<THooks>> {\n // 1. Validate — check for empty/whitespace IDs and duplicates\n const seen = new Set<string>();\n for (const plugin of plugins) {\n if (!plugin.id || !plugin.id.trim()) {\n throw new Error('Plugin config error: \"id\" is required');\n }\n if (plugin.id !== plugin.id.trim()) {\n throw new Error(\n `Plugin config error: \"id\" must not have leading or trailing whitespace (got: \"${plugin.id}\")`,\n );\n }\n if (seen.has(plugin.id)) {\n throw new Error(`Duplicate plugin ID: \"${plugin.id}\"`);\n }\n seen.add(plugin.id);\n }\n\n // 2. Register hooks\n const hookRegistry = new HookRegistry<THooks>();\n for (const plugin of plugins) {\n if (plugin.hooks) {\n hookRegistry.register(plugin.id, plugin.hooks);\n }\n }\n\n // 3. Collect endpoints (annotated with pluginId)\n const endpoints: ResolvedEndpoint[] = [];\n for (const plugin of plugins) {\n if (plugin.endpoints) {\n for (const ep of plugin.endpoints) {\n if (!ep.path || !ep.path.startsWith(\"/\")) {\n throw new Error(\n `Plugin \"${plugin.id}\" endpoint path must start with \"/\" (got: \"${ep.path}\")`,\n );\n }\n endpoints.push({ ...ep, pluginId: plugin.id });\n }\n }\n }\n\n // 4. Merge schemas\n const schema = mergeSchemas(plugins);\n\n // 5. Init — sequential in declaration order\n const pluginIds = plugins.map((p) => p.id);\n const frozenConfig = Object.freeze({ ...config });\n const context: PluginContext = {\n productId,\n hasPlugin: (id: string) => seen.has(id),\n config: frozenConfig,\n };\n\n for (const plugin of plugins) {\n if (plugin.init) {\n try {\n await plugin.init(context);\n } catch (err) {\n throw new PluginInitError(plugin.id, err);\n }\n }\n }\n\n return { hookRegistry, endpoints, schema, pluginIds };\n}\n"],"mappings":";AAgBO,IAAM,oBAAN,cAAgC,MAAM;AAAA,EACzB,OAAO;AAAA,EAChB;AAAA,EACA;AAAA,EAET,YAAY,UAAkB,MAAc,OAAgB;AAC1D,UAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACpE,UAAM,WAAW,QAAQ,qBAAqB,IAAI,MAAM,MAAM,IAAI;AAAA,MAChE;AAAA,IACF,CAAC;AACD,SAAK,WAAW;AAChB,SAAK,OAAO;AAAA,EACd;AACF;AAkBO,IAAM,eAAN,MAAgD;AAAA,EAC7C,QAAQ,oBAAI,IAA8B;AAAA,EAC1C,oBAAoB,oBAAI,IAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ5C,SAAS,UAAkB,OAA4C;AACrE,QAAI,KAAK,kBAAkB,IAAI,QAAQ,GAAG;AACxC,YAAM,IAAI,MAAM,WAAW,QAAQ,yBAAyB;AAAA,IAC9D;AACA,SAAK,kBAAkB,IAAI,QAAQ;AAEnC,eAAW,CAAC,KAAK,OAAO,KAAK,OAAO,QAAQ,KAAK,GAAG;AAClD,UAAI,OAAO,YAAY,YAAY;AACjC;AAAA,MACF;AAEA,UAAI,OAAO,KAAK,MAAM,IAAI,GAAG;AAC7B,UAAI,SAAS,QAAW;AACtB,eAAO,CAAC;AACR,aAAK,MAAM,IAAI,KAAK,IAAI;AAAA,MAC1B;AACA,WAAK,KAAK,EAAE,UAAU,QAAQ,CAAC;AAAA,IACjC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,aACJ,MACA,SACe;AACf,UAAM,OAAO,KAAK,MAAM,IAAI,IAAI;AAChC,QAAI,SAAS,QAAW;AACtB;AAAA,IACF;AACA,UAAM,WAAW,CAAC,GAAG,IAAI;AACzB,eAAW,SAAS,UAAU;AAC5B,UAAI;AACF,cAAM,KAAK,YAAY,MAAM,SAAS,OAAO;AAAA,MAC/C,SAAS,OAAgB;AACvB,cAAM,IAAI,kBAAkB,MAAM,UAAU,MAAM,KAAK;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,YACJ,MACA,SACwB;AACxB,UAAM,OAAO,KAAK,MAAM,IAAI,IAAI;AAChC,QAAI,SAAS,QAAW;AACtB,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,SAAwB,CAAC;AAC/B,UAAM,WAAW,CAAC,GAAG,IAAI;AACzB,eAAW,SAAS,UAAU;AAC5B,UAAI;AACF,cAAM,KAAK,YAAY,MAAM,SAAS,OAAO;AAAA,MAC/C,SAAS,OAAgB;AACvB,eAAO,KAAK;AAAA,UACV,UAAU,MAAM;AAAA,UAChB;AAAA,UACA;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,YACN,SACA,SACsB;AACtB,WAAQ,QAAmD,OAAO;AAAA,EACpE;AACF;;;ACrJO,SAAS,cACd,OACA,MACoB;AACpB,MAAI,UAAU,QAAW;AACvB,UAAM,IAAI,MAAM,yBAAyB,IAAI,eAAe;AAAA,EAC9D;AACF;AAMO,SAAS,YAAiD,OAAU,SAAc,MAAoB;AAC3G,MAAI,CAAC,QAAQ,SAAS,KAAK,GAAG;AAC5B,UAAM,aAAa,QAAQ,KAAK,IAAI;AACpC,UAAM,IAAI;AAAA,MACR,yBAAyB,IAAI,qBAAqB,UAAU,UAAU,OAAO,KAAK,CAAC;AAAA,IACrF;AAAA,EACF;AACF;;;ACCO,SAAS,aACd,SACoB;AACpB,QAAM,SAAgD,CAAC;AACvD,QAAM,SAAgE,CAAC;AAEvE,QAAM,cAAc,oBAAI,IAAoB;AAC5C,QAAM,cAAc,oBAAI,IAAoB;AAE5C,aAAW,UAAU,SAAS;AAC5B,QAAI,CAAC,OAAO,QAAQ;AAClB;AAAA,IACF;AAEA,QAAI,OAAO,OAAO,QAAQ;AACxB,iBAAW,CAAC,WAAW,QAAQ,KAAK,OAAO;AAAA,QACzC,OAAO,OAAO;AAAA,MAChB,GAAG;AACD,cAAM,gBAAgB,YAAY,IAAI,SAAS;AAC/C,YAAI,kBAAkB,QAAW;AAC/B,gBAAM,IAAI;AAAA,YACR,2BAA2B,SAAS,sBAAsB,aAAa,UAAU,OAAO,EAAE;AAAA,UAC5F;AAAA,QACF;AACA,oBAAY,IAAI,WAAW,OAAO,EAAE;AACpC,eAAO,SAAS,IAAI;AAAA,MACtB;AAAA,IACF;AAEA,QAAI,OAAO,OAAO,QAAQ;AACxB,iBAAW,CAAC,WAAW,MAAM,KAAK,OAAO,QAAQ,OAAO,OAAO,MAAM,GAAG;AACtE,YAAI,kBAAkB,OAAO,SAAS;AACtC,YAAI,oBAAoB,QAAW;AACjC,4BAAkB,CAAC;AACnB,iBAAO,SAAS,IAAI;AAAA,QACtB;AAEA,mBAAW,CAAC,WAAW,QAAQ,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC1D,gBAAM,MAAM,GAAG,SAAS,IAAI,SAAS;AACrC,gBAAM,gBAAgB,YAAY,IAAI,GAAG;AACzC,cAAI,kBAAkB,QAAW;AAC/B,kBAAM,IAAI;AAAA,cACR,2BAA2B,SAAS,IAAI,SAAS,uBAAuB,aAAa,UAAU,OAAO,EAAE;AAAA,YAC1G;AAAA,UACF;AACA,sBAAY,IAAI,KAAK,OAAO,EAAE;AAC9B,0BAAgB,SAAS,IAAI;AAAA,QAC/B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,QAAQ,OAAO;AAC1B;;;ACpEO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EACvB,OAAO;AAAA,EAChB;AAAA,EAET,YAAY,UAAkB,OAAgB;AAC5C,UAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACpE,UAAM,WAAW,QAAQ,yBAAyB,MAAM,IAAI,EAAE,MAAM,CAAC;AACrE,SAAK,WAAW;AAAA,EAClB;AACF;AA8BA,eAAsB,eACpB,WACA,SACA,QACkC;AAElC,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,UAAU,SAAS;AAC5B,QAAI,CAAC,OAAO,MAAM,CAAC,OAAO,GAAG,KAAK,GAAG;AACnC,YAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AACA,QAAI,OAAO,OAAO,OAAO,GAAG,KAAK,GAAG;AAClC,YAAM,IAAI;AAAA,QACR,iFAAiF,OAAO,EAAE;AAAA,MAC5F;AAAA,IACF;AACA,QAAI,KAAK,IAAI,OAAO,EAAE,GAAG;AACvB,YAAM,IAAI,MAAM,yBAAyB,OAAO,EAAE,GAAG;AAAA,IACvD;AACA,SAAK,IAAI,OAAO,EAAE;AAAA,EACpB;AAGA,QAAM,eAAe,IAAI,aAAqB;AAC9C,aAAW,UAAU,SAAS;AAC5B,QAAI,OAAO,OAAO;AAChB,mBAAa,SAAS,OAAO,IAAI,OAAO,KAAK;AAAA,IAC/C;AAAA,EACF;AAGA,QAAM,YAAgC,CAAC;AACvC,aAAW,UAAU,SAAS;AAC5B,QAAI,OAAO,WAAW;AACpB,iBAAW,MAAM,OAAO,WAAW;AACjC,YAAI,CAAC,GAAG,QAAQ,CAAC,GAAG,KAAK,WAAW,GAAG,GAAG;AACxC,gBAAM,IAAI;AAAA,YACR,WAAW,OAAO,EAAE,8CAA8C,GAAG,IAAI;AAAA,UAC3E;AAAA,QACF;AACA,kBAAU,KAAK,EAAE,GAAG,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MAC/C;AAAA,IACF;AAAA,EACF;AAGA,QAAM,SAAS,aAAa,OAAO;AAGnC,QAAM,YAAY,QAAQ,IAAI,CAAC,MAAM,EAAE,EAAE;AACzC,QAAM,eAAe,OAAO,OAAO,EAAE,GAAG,OAAO,CAAC;AAChD,QAAM,UAAyB;AAAA,IAC7B;AAAA,IACA,WAAW,CAAC,OAAe,KAAK,IAAI,EAAE;AAAA,IACtC,QAAQ;AAAA,EACV;AAEA,aAAW,UAAU,SAAS;AAC5B,QAAI,OAAO,MAAM;AACf,UAAI;AACF,cAAM,OAAO,KAAK,OAAO;AAAA,MAC3B,SAAS,KAAK;AACZ,cAAM,IAAI,gBAAgB,OAAO,IAAI,GAAG;AAAA,MAC1C;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,cAAc,WAAW,QAAQ,UAAU;AACtD;","names":[]}
package/dist/schema.d.ts CHANGED
@@ -11,8 +11,10 @@ export interface MergedPluginSchema {
11
11
  /**
12
12
  * Merges schema declarations from all plugins into a single result.
13
13
  *
14
- * Returns empty maps. Full merge logic (conflict detection, ordering)
15
- * will be added when schema merging is needed.
14
+ * Iterates plugins in declaration order and collects new tables and
15
+ * extended fields. Throws on conflicts:
16
+ * - Two plugins defining the same table name
17
+ * - Two plugins extending the same field on the same table
16
18
  */
17
- export declare function mergeSchemas<THooks extends HookContract>(_plugins: PluginDefinition<THooks>[]): MergedPluginSchema;
19
+ export declare function mergeSchemas<THooks extends HookContract>(plugins: PluginDefinition<THooks>[]): MergedPluginSchema;
18
20
  //# sourceMappingURL=schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAChB,qBAAqB,EACrB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAEpB;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;IAC9C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC;CAC/D;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,SAAS,YAAY,EACtD,QAAQ,EAAE,gBAAgB,CAAC,MAAM,CAAC,EAAE,GACnC,kBAAkB,CAEpB"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAChB,qBAAqB,EACrB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAEpB;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;IAC9C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC;CAC/D;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,MAAM,SAAS,YAAY,EACtD,OAAO,EAAE,gBAAgB,CAAC,MAAM,CAAC,EAAE,GAClC,kBAAkB,CAmDpB"}
package/dist/types.d.ts CHANGED
@@ -1,93 +1,2 @@
1
- /**
2
- * A contract mapping hook names to their payload shapes.
3
- * Products define this to declare what hooks are available.
4
- *
5
- * Example:
6
- * ```ts
7
- * type MyHooks = {
8
- * "tenant:created": { tenantId: string; name: string };
9
- * "tenant:deleted": { tenantId: string };
10
- * };
11
- * ```
12
- */
13
- export type HookContract = Record<string, Record<string, unknown>>;
14
- /**
15
- * Derives handler function signatures from a HookContract.
16
- * Each key becomes a handler that receives the corresponding payload.
17
- */
18
- export type HookHandlers<T extends HookContract> = {
19
- [K in keyof T]: (payload: T[K]) => void | Promise<void>;
20
- };
21
- /**
22
- * Runtime context passed to plugin init functions.
23
- * Provides product identity, peer plugin awareness, and configuration.
24
- */
25
- export interface PluginContext {
26
- productId: string;
27
- hasPlugin: (id: string) => boolean;
28
- config: Record<string, unknown>;
29
- }
30
- /** Common database column types that plugins can declare. */
31
- export type FieldType = "string" | "number" | "boolean" | "date" | "json";
32
- /** A single column definition within a plugin table. */
33
- export interface PluginFieldDefinition {
34
- type: FieldType;
35
- required?: boolean;
36
- unique?: boolean;
37
- defaultValue?: string | number | boolean | null | (() => unknown);
38
- references?: {
39
- table: string;
40
- field: string;
41
- onDelete?: "cascade" | "restrict" | "set-null" | "no-action";
42
- };
43
- index?: boolean;
44
- }
45
- /** A single table definition declared by a plugin. Keyed by field name. */
46
- export interface PluginTableDefinition {
47
- fields: Record<string, PluginFieldDefinition>;
48
- }
49
- /**
50
- * Schema declaration for a plugin.
51
- * `tables` declares new tables (keyed by table name).
52
- * `extend` adds fields to existing product tables (keyed by table name, then field name).
53
- */
54
- export interface PluginSchema {
55
- tables?: Record<string, PluginTableDefinition>;
56
- extend?: Record<string, Record<string, PluginFieldDefinition>>;
57
- }
58
- /** HTTP methods that plugin endpoints can handle. */
59
- export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
60
- /** Incoming request context provided to endpoint handlers. */
61
- export interface PluginRequestContext {
62
- request: Request;
63
- params: Record<string, string>;
64
- body: unknown;
65
- hasPlugin: (id: string) => boolean;
66
- }
67
- /** Response shape returned by endpoint handlers. */
68
- export interface PluginResponse {
69
- status: number;
70
- body: unknown;
71
- }
72
- /** Handler function signature for plugin endpoints. */
73
- export type PluginEndpointHandler = (context: PluginRequestContext) => Promise<PluginResponse>;
74
- /** A single HTTP endpoint declared by a plugin. */
75
- export interface PluginEndpoint {
76
- method: HttpMethod;
77
- path: string;
78
- handler: PluginEndpointHandler;
79
- }
80
- /**
81
- * Full plugin definition. Generic over the hook contract so plugins
82
- * get type-safe hook handlers when a product provides its contract.
83
- */
84
- export interface PluginDefinition<THooks extends HookContract = HookContract> {
85
- id: string;
86
- init?: (context: PluginContext) => void | Promise<void>;
87
- hooks?: Partial<HookHandlers<THooks>>;
88
- schema?: PluginSchema;
89
- endpoints?: PluginEndpoint[];
90
- /** Type-level brand for inference. Not used at runtime. */
91
- $Infer?: Record<string, unknown>;
92
- }
1
+ export type { HookContract, HookHandlers, PluginContext, FieldType, PluginFieldDefinition, PluginTableDefinition, PluginSchema, HttpMethod, PluginRequestContext, PluginResponse, PluginEndpointHandler, PluginEndpoint, PluginDefinition, } from "@usebetterdev/contract/plugin-types";
93
2
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,SAAS,EACT,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,EACZ,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,qBAAqB,EACrB,cAAc,EACd,gBAAgB,GACjB,MAAM,qCAAqC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usebetterdev/plugin",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "repository": "github:usebetter-dev/usebetter",
5
5
  "bugs": "https://github.com/usebetter-dev/usebetter/issues",
6
6
  "homepage": "https://github.com/usebetter-dev/usebetter#readme",
@@ -17,12 +17,21 @@
17
17
  "types": "./dist/index.d.ts",
18
18
  "import": "./dist/index.js",
19
19
  "require": "./dist/index.cjs"
20
+ },
21
+ "./config": {
22
+ "types": "./dist/config.d.ts",
23
+ "import": "./dist/config.js",
24
+ "require": "./dist/config.cjs"
20
25
  }
21
26
  },
22
27
  "files": [
23
28
  "dist",
24
29
  "README.md"
25
30
  ],
31
+ "dependencies": {
32
+ "c12": "^2.0.1",
33
+ "@usebetterdev/contract": "0.6.0"
34
+ },
26
35
  "devDependencies": {
27
36
  "@types/node": "^22.10.0",
28
37
  "tsup": "^8.3.5",