|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * [#29986 codemod] The "Action system and packaged-action activation" section |
| 5 | + * of `ObjectQL`: the bodies of `registerAction`, `executeAction`, |
| 6 | + * `listRegisteredActions`, `removeActionsByPackage`, |
| 7 | + * `setActionActivationStore`, `hydrateActionActivations`, `isActionEnabled`, |
| 8 | + * `listDisabledActions`, `getDisabledActions`, `describeDisabledAction`, |
| 9 | + * `setActionActive` moved out of `engine.ts` whole by |
| 10 | + * `scripts/codemod-split-engine.mjs`. |
| 11 | + * |
| 12 | + * A MOVE, not a redesign. {@link ActionsFacet} is a stateless facet of `ObjectQL` |
| 13 | + * (ruling 6624345766): each static method of it is the body of the `ObjectQL` |
| 14 | + * method of the same name exactly as it stood: not re-indented, not |
| 15 | + * formatted, not edited. The method stays on the class as a one-call stub with |
| 16 | + * its signature, so the published surface, an instance spy and a subclass |
| 17 | + * override are what they were. A public or protected method keeps its |
| 18 | + * docblock on the class, the contract; a private method's docblock moved here |
| 19 | + * with its body. The state stays on `ObjectQL`. |
| 20 | + * |
| 21 | + * The bodies reach the class through {@link ActionsHost}: exactly the |
| 22 | + * `ObjectQL` members they read, private ones included. Each stub hands its |
| 23 | + * body one literal of the members that body reads, plus a `.call`: methods go |
| 24 | + * as closures that call them on the engine at call time, so a subclass |
| 25 | + * override or an instance spy is still the one reached; fields and accessors |
| 26 | + * go as GETTERS, read when the body reads them, as they were on the class, |
| 27 | + * and a field a body assigns goes with a SETTER that assigns it on the engine. |
| 28 | + * |
| 29 | + * The module imports `engine.ts` for types only, so the two modules form no |
| 30 | + * evaluation cycle. `index.ts` re-exports nothing from this module. |
| 31 | + */ |
| 32 | + |
| 33 | +import type { ActionActivationRow, ActionActivationStore, ActionActivationWriteOptions } from './action-activation.js'; |
| 34 | +import type { ObjectQL } from './engine.js'; |
| 35 | + |
| 36 | +/** |
| 37 | + * The `ObjectQL` members the bodies of {@link ActionsFacet} read — every one of |
| 38 | + * them, and no other. Each is typed AS the class member (an indexed access on |
| 39 | + * the class), so a change to a member's signature on the class is a change |
| 40 | + * here too. `readonly` members are the getters; the others are fields a body |
| 41 | + * assigns. |
| 42 | + */ |
| 43 | +export interface ActionsHost { |
| 44 | + readonly actionActivation: ObjectQL['actionActivation']; |
| 45 | + readonly actions: ObjectQL['actions']; |
| 46 | + readonly logger: ObjectQL['logger']; |
| 47 | +} |
| 48 | + |
| 49 | +/** The bodies of the "Action system and packaged-action activation" section of `ObjectQL`: a stateless facet, called by the class's stubs. */ |
| 50 | +export class ActionsFacet { |
| 51 | + /** The body of `ObjectQL#registerAction`; the method's docblock there is its contract. */ |
| 52 | + static registerAction(this: Pick<ActionsHost, 'actions' | 'logger'>, objectName: string, actionName: string, handler: (ctx: any) => Promise<any> | any, packageName?: string): void { |
| 53 | + const key = `${objectName}:${actionName}`; |
| 54 | + const replaced = this.actions.get(key); |
| 55 | + if (replaced !== undefined && replaced.package !== packageName) { |
| 56 | + const by = (name: string | undefined) => (name ? `by package '${name}'` : 'with no package'); |
| 57 | + this.logger.warn( |
| 58 | + `Action handler '${key}' registered ${by(packageName)} replaces the one registered ${by(replaced.package)}: ` |
| 59 | + + `the action door now runs the handler registered ${by(packageName)}, whichever declaration object ` |
| 60 | + + `'${objectName}' lists${packageName ? `, and withdrawing package '${packageName}' leaves the action with no handler` : ''}. ` |
| 61 | + + `Two owners share one handler key only when both declare action '${actionName}' on object ` |
| 62 | + + `'${objectName}' — rename one of them.`, |
| 63 | + { objectName, actionName, package: packageName, replacedPackage: replaced.package }, |
| 64 | + ); |
| 65 | + } |
| 66 | + this.actions.set(key, { handler, package: packageName }); |
| 67 | + this.logger.debug('Registered action', { objectName, actionName, package: packageName }); |
| 68 | + } |
| 69 | + |
| 70 | + /** The body of `ObjectQL#executeAction`; the method's docblock there is its contract. */ |
| 71 | + static async executeAction(this: Pick<ActionsHost, 'actions'>, objectName: string, actionName: string, ctx: any): Promise<any> { |
| 72 | + const entry = this.actions.get(`${objectName}:${actionName}`); |
| 73 | + if (!entry) { |
| 74 | + throw new Error(`Action '${actionName}' on object '${objectName}' not found`); |
| 75 | + } |
| 76 | + return entry.handler(ctx); |
| 77 | + } |
| 78 | + |
| 79 | + /** The body of `ObjectQL#listRegisteredActions`; the method's docblock there is its contract. */ |
| 80 | + static listRegisteredActions(this: Pick<ActionsHost, 'actions'>): Array<{ objectName: string; actionName: string; package?: string }> { |
| 81 | + const out: Array<{ objectName: string; actionName: string; package?: string }> = []; |
| 82 | + for (const [key, entry] of this.actions.entries()) { |
| 83 | + const sep = key.indexOf(':'); |
| 84 | + if (sep < 0) continue; |
| 85 | + out.push({ |
| 86 | + objectName: key.slice(0, sep), |
| 87 | + actionName: key.slice(sep + 1), |
| 88 | + ...(entry.package ? { package: entry.package } : {}), |
| 89 | + }); |
| 90 | + } |
| 91 | + return out; |
| 92 | + } |
| 93 | + |
| 94 | + /** The body of `ObjectQL#removeActionsByPackage`; the method's docblock there is its contract. */ |
| 95 | + static removeActionsByPackage(this: Pick<ActionsHost, 'actions'>, packageName: string): void { |
| 96 | + for (const [key, entry] of this.actions.entries()) { |
| 97 | + if (entry.package === packageName) { |
| 98 | + this.actions.delete(key); |
| 99 | + } |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + /** The body of `ObjectQL#setActionActivationStore`; the method's docblock there is its contract. */ |
| 104 | + static setActionActivationStore(this: Pick<ActionsHost, 'actionActivation'>, store: ActionActivationStore): void { |
| 105 | + this.actionActivation.attach(store); |
| 106 | + } |
| 107 | + |
| 108 | + /** The body of `ObjectQL#hydrateActionActivations`; the method's docblock there is its contract. */ |
| 109 | + static async hydrateActionActivations(this: Pick<ActionsHost, 'actionActivation'>): Promise<string[]> { |
| 110 | + return this.actionActivation.hydrate(); |
| 111 | + } |
| 112 | + |
| 113 | + /** The body of `ObjectQL#isActionEnabled`; the method's docblock there is its contract. */ |
| 114 | + static isActionEnabled(this: Pick<ActionsHost, 'actionActivation'>, actionName: string): boolean { |
| 115 | + return this.actionActivation.isEnabled(actionName); |
| 116 | + } |
| 117 | + |
| 118 | + /** The body of `ObjectQL#listDisabledActions`; the method's docblock there is its contract. */ |
| 119 | + static listDisabledActions(this: Pick<ActionsHost, 'actionActivation'>): string[] { |
| 120 | + return this.actionActivation.disabledNames(); |
| 121 | + } |
| 122 | + |
| 123 | + /** The body of `ObjectQL#getDisabledActions`; the method's docblock there is its contract. */ |
| 124 | + static getDisabledActions(this: Pick<ActionsHost, 'actionActivation'>): string[] | undefined { |
| 125 | + return this.actionActivation.durable ? this.actionActivation.disabledNames() : undefined; |
| 126 | + } |
| 127 | + |
| 128 | + /** The body of `ObjectQL#describeDisabledAction`; the method's docblock there is its contract. */ |
| 129 | + static describeDisabledAction(this: Pick<ActionsHost, 'actionActivation' | 'logger'>, actionName: string): string { |
| 130 | + this.logger.info( |
| 131 | + `ACTION_DISABLED: packaged action '${actionName}' refused — it is switched off in the activation ledger ` + |
| 132 | + '(sys_metadata_activation, ADR-0126 §8 item 2).', |
| 133 | + { action: actionName, refusal: 'ACTION_DISABLED' }, |
| 134 | + ); |
| 135 | + return this.actionActivation.describeDisabled(actionName); |
| 136 | + } |
| 137 | + |
| 138 | + /** The body of `ObjectQL#setActionActive`; the method's docblock there is its contract. */ |
| 139 | + static async setActionActive(this: Pick<ActionsHost, 'actionActivation'>, row: ActionActivationRow, options?: ActionActivationWriteOptions): Promise<void> { |
| 140 | + await this.actionActivation.setActive(row, options); |
| 141 | + } |
| 142 | +} |
0 commit comments