@usebetterdev/plugin 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +9 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/resolve-plugins.d.ts.map +1 -1
- package/dist/schema.d.ts +7 -2
- package/dist/schema.d.ts.map +1 -1
- package/package.json +3 -3
- package/dist/demo/auth-events.d.ts +0 -34
- package/dist/demo/auth-events.d.ts.map +0 -1
- package/dist/demo/webhook-dispatcher.d.ts +0 -74
- package/dist/demo/webhook-dispatcher.d.ts.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -232,6 +232,7 @@ async function resolvePlugins(productId, plugins, config) {
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
const endpoints = [];
|
|
235
|
+
const endpointOwners = /* @__PURE__ */ new Map();
|
|
235
236
|
for (const plugin of plugins) {
|
|
236
237
|
if (plugin.endpoints) {
|
|
237
238
|
for (const ep of plugin.endpoints) {
|
|
@@ -240,6 +241,14 @@ async function resolvePlugins(productId, plugins, config) {
|
|
|
240
241
|
`Plugin "${plugin.id}" endpoint path must start with "/" (got: "${ep.path}")`
|
|
241
242
|
);
|
|
242
243
|
}
|
|
244
|
+
const key = `${ep.method} ${ep.path}`;
|
|
245
|
+
const existingOwner = endpointOwners.get(key);
|
|
246
|
+
if (existingOwner !== void 0) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`Endpoint conflict: "${key}" registered by plugin "${existingOwner}" and "${plugin.id}"`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
endpointOwners.set(key, plugin.id);
|
|
243
252
|
endpoints.push({ ...ep, pluginId: plugin.id });
|
|
244
253
|
}
|
|
245
254
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -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\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":[]}
|
|
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, SchemaSource } 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 PluginFieldDefinition,\n PluginSchema,\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/** Minimal plugin shape needed by mergeSchemas — only id and schema. */\nexport interface SchemaSource {\n id: string;\n schema?: PluginSchema;\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(\n plugins: ReadonlyArray<SchemaSource>,\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 const endpointOwners = new Map<string, string>();\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 const key = `${ep.method} ${ep.path}`;\n const existingOwner = endpointOwners.get(key);\n if (existingOwner !== undefined) {\n throw new Error(\n `Endpoint conflict: \"${key}\" registered by plugin \"${existingOwner}\" and \"${plugin.id}\"`,\n );\n }\n endpointOwners.set(key, plugin.id);\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
|
@@ -3,7 +3,7 @@ export { HookRegistry, PluginInvokeError } from "./hook-registry.js";
|
|
|
3
3
|
export type { PluginError } from "./hook-registry.js";
|
|
4
4
|
export { assertDefined, assertOneOf } from "./validation.js";
|
|
5
5
|
export { mergeSchemas } from "./schema.js";
|
|
6
|
-
export type { MergedPluginSchema } from "./schema.js";
|
|
6
|
+
export type { MergedPluginSchema, SchemaSource } from "./schema.js";
|
|
7
7
|
export { resolvePlugins, PluginInitError } from "./resolve-plugins.js";
|
|
8
8
|
export type { ResolvedPlugins, ResolvedEndpoint } from "./resolve-plugins.js";
|
|
9
9
|
export type { BetterConfig, BetterTenantStaticConfig, BetterAuditStaticConfig, BetterConsoleStaticConfig, } from "./config-types.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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,YAAY,EAAE,MAAM,aAAa,CAAC;AAEpE,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
|
@@ -200,6 +200,7 @@ async function resolvePlugins(productId, plugins, config) {
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
const endpoints = [];
|
|
203
|
+
const endpointOwners = /* @__PURE__ */ new Map();
|
|
203
204
|
for (const plugin of plugins) {
|
|
204
205
|
if (plugin.endpoints) {
|
|
205
206
|
for (const ep of plugin.endpoints) {
|
|
@@ -208,6 +209,14 @@ async function resolvePlugins(productId, plugins, config) {
|
|
|
208
209
|
`Plugin "${plugin.id}" endpoint path must start with "/" (got: "${ep.path}")`
|
|
209
210
|
);
|
|
210
211
|
}
|
|
212
|
+
const key = `${ep.method} ${ep.path}`;
|
|
213
|
+
const existingOwner = endpointOwners.get(key);
|
|
214
|
+
if (existingOwner !== void 0) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Endpoint conflict: "${key}" registered by plugin "${existingOwner}" and "${plugin.id}"`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
endpointOwners.set(key, plugin.id);
|
|
211
220
|
endpoints.push({ ...ep, pluginId: plugin.id });
|
|
212
221
|
}
|
|
213
222
|
}
|
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 * 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":[]}
|
|
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 PluginFieldDefinition,\n PluginSchema,\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/** Minimal plugin shape needed by mergeSchemas — only id and schema. */\nexport interface SchemaSource {\n id: string;\n schema?: PluginSchema;\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(\n plugins: ReadonlyArray<SchemaSource>,\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 const endpointOwners = new Map<string, string>();\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 const key = `${ep.method} ${ep.path}`;\n const existingOwner = endpointOwners.get(key);\n if (existingOwner !== undefined) {\n throw new Error(\n `Endpoint conflict: \"${key}\" registered by plugin \"${existingOwner}\" and \"${plugin.id}\"`,\n );\n }\n endpointOwners.set(key, plugin.id);\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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-plugins.d.ts","sourceRoot":"","sources":["../src/resolve-plugins.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,KAAK,EACV,YAAY,EAEZ,gBAAgB,EAChB,cAAc,EACf,MAAM,YAAY,CAAC;AAEpB,qBAAa,eAAgB,SAAQ,KAAK;IACxC,SAAkB,IAAI,qBAAqB;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAEd,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;CAK7C;AAED;;GAEG;AACH,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe,CAAC,MAAM,SAAS,YAAY;IAC1D,YAAY,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IACnC,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,MAAM,SAAS,YAAY,EAC9D,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,gBAAgB,CAAC,MAAM,CAAC,EAAE,EACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"resolve-plugins.d.ts","sourceRoot":"","sources":["../src/resolve-plugins.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,KAAK,EACV,YAAY,EAEZ,gBAAgB,EAChB,cAAc,EACf,MAAM,YAAY,CAAC;AAEpB,qBAAa,eAAgB,SAAQ,KAAK;IACxC,SAAkB,IAAI,qBAAqB;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAEd,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;CAK7C;AAED;;GAEG;AACH,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe,CAAC,MAAM,SAAS,YAAY;IAC1D,YAAY,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IACnC,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,MAAM,SAAS,YAAY,EAC9D,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,gBAAgB,CAAC,MAAM,CAAC,EAAE,EACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAyElC"}
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PluginFieldDefinition, PluginSchema, PluginTableDefinition } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Merged schema produced by combining all plugin schema declarations.
|
|
4
4
|
* Unlike per-plugin `PluginSchema` (where fields are optional),
|
|
@@ -8,6 +8,11 @@ export interface MergedPluginSchema {
|
|
|
8
8
|
tables: Record<string, PluginTableDefinition>;
|
|
9
9
|
extend: Record<string, Record<string, PluginFieldDefinition>>;
|
|
10
10
|
}
|
|
11
|
+
/** Minimal plugin shape needed by mergeSchemas — only id and schema. */
|
|
12
|
+
export interface SchemaSource {
|
|
13
|
+
id: string;
|
|
14
|
+
schema?: PluginSchema;
|
|
15
|
+
}
|
|
11
16
|
/**
|
|
12
17
|
* Merges schema declarations from all plugins into a single result.
|
|
13
18
|
*
|
|
@@ -16,5 +21,5 @@ export interface MergedPluginSchema {
|
|
|
16
21
|
* - Two plugins defining the same table name
|
|
17
22
|
* - Two plugins extending the same field on the same table
|
|
18
23
|
*/
|
|
19
|
-
export declare function mergeSchemas
|
|
24
|
+
export declare function mergeSchemas(plugins: ReadonlyArray<SchemaSource>): MergedPluginSchema;
|
|
20
25
|
//# sourceMappingURL=schema.d.ts.map
|
package/dist/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,YAAY,EACZ,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,wEAAwE;AACxE,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,aAAa,CAAC,YAAY,CAAC,GACnC,kBAAkB,CAmDpB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usebetterdev/plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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",
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"c12": "^2.0.1",
|
|
33
|
-
"@usebetterdev/contract": "0.
|
|
33
|
+
"@usebetterdev/contract": "0.8.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^22.10.0",
|
|
37
37
|
"tsup": "^8.3.5",
|
|
38
38
|
"typescript": "~5.7.2",
|
|
39
39
|
"vitest": "^2.1.6",
|
|
40
|
-
"@usebetterdev/test-utils": "^0.5.
|
|
40
|
+
"@usebetterdev/test-utils": "^0.5.4"
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|
|
43
43
|
"node": ">=22"
|
|
@@ -1,34 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"auth-events.d.ts","sourceRoot":"","sources":["../../src/demo/auth-events.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsBtB,CAAC"}
|
|
@@ -1,74 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|