|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * [#25467 cut 10] The served lock state and the stored-row decline of |
| 5 | + * `ObjectStackProtocolImplementation` — the read envelope's lock derivation |
| 6 | + * (`servedLockState`), the shipped-flow-name predicate and its registry half |
| 7 | + * (`isShippedFlowName`, `isStoredFlowEntryOfShippedName`), the one by-name |
| 8 | + * decision the active reads take before they serve a stored row |
| 9 | + * (`declinesStoredRow`), its sealed-overlay limb (`isUnservedSealedOverlay`) |
| 10 | + * and its registry half (`isStoredEntryOfDeclinedName`) — moved out of |
| 11 | + * `protocol.ts` whole. |
| 12 | + * |
| 13 | + * A MOVE, not a redesign. Each function below is the body of the |
| 14 | + * `ObjectStackProtocolImplementation` method of the same name as it stood, |
| 15 | + * re-indented one level and otherwise unedited: the same reads, in the same |
| 16 | + * order, throwing exactly where they did with the same message. |
| 17 | + * |
| 18 | + * ## How the moved bodies reach the class |
| 19 | + * |
| 20 | + * The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts` |
| 21 | + * header carries the full reasoning), as `protocol-batch-data.ts` applies it. |
| 22 | + * The class keeps every method — the published `dist/*.d.ts` lists each one's |
| 23 | + * name, docblock and declared signature — and each body is one literal of |
| 24 | + * exactly the {@link LockStateHost} members that method reads, plus a |
| 25 | + * `.call`. A method is handed over as a closure that calls it on the protocol |
| 26 | + * at call time, so a test double or spy set on the instance is still the one |
| 27 | + * the body reaches, and the moved bodies that call one another do so through |
| 28 | + * the protocol, exactly as before. Every other reader of these six methods |
| 29 | + * keeps calling the class method, the REST and runtime published doors among |
| 30 | + * them. |
| 31 | + * |
| 32 | + * The bodies read no field, so the host carries no getter. |
| 33 | + * |
| 34 | + * The one module-level declaration of `protocol.ts` the bodies read, |
| 35 | + * `canonicalMetaType`, stays there, where code that did not move reads it too, |
| 36 | + * and is imported from there: the module cycle is evaluation-safe because it |
| 37 | + * is read only inside a function body, never while the two modules are first |
| 38 | + * evaluated. Everything else is imported from the module `protocol.ts` |
| 39 | + * imports it from. Nothing re-exports this module, so the published surface |
| 40 | + * does not move. |
| 41 | + */ |
| 42 | + |
| 43 | +import type { ObjectStackProtocolImplementation } from './protocol.js'; |
| 44 | +import { canonicalMetaType } from './protocol.js'; |
| 45 | +import type { ItemLock } from './item-lock.js'; |
| 46 | +import { isOriginGatedType } from './packaged-base-regime.js'; |
| 47 | +import { isCodeArtifactBody } from '@objectstack/metadata-core'; |
| 48 | +import { PLURAL_TO_SINGULAR } from '@objectstack/spec/shared'; |
| 49 | +import { |
| 50 | + extractProtection, |
| 51 | + evaluateLockForWrite, |
| 52 | + evaluateLockForDelete, |
| 53 | + MetadataLockSchema, |
| 54 | +} from '@objectstack/spec/kernel'; |
| 55 | +import type { resolveLockState } from '@objectstack/spec/kernel'; |
| 56 | + |
| 57 | +/** |
| 58 | + * The `ObjectStackProtocolImplementation` members the functions below read — |
| 59 | + * every one of them, and no other; each function's `this` is the `Pick` of the |
| 60 | + * members IT reads. Each is typed AS the class member (an indexed access on |
| 61 | + * the class), so a change to a member's signature on the class is a change |
| 62 | + * here too, and naming a member the class does not have is a compile error. |
| 63 | + */ |
| 64 | +export interface LockStateHost { |
| 65 | + declinesStoredRow: ObjectStackProtocolImplementation['declinesStoredRow']; |
| 66 | + isDeclaredCodeDatasource: ObjectStackProtocolImplementation['isDeclaredCodeDatasource']; |
| 67 | + isSealedManagedItem: ObjectStackProtocolImplementation['isSealedManagedItem']; |
| 68 | + isShippedFlowName: ObjectStackProtocolImplementation['isShippedFlowName']; |
| 69 | + isUnservedSealedOverlay: ObjectStackProtocolImplementation['isUnservedSealedOverlay']; |
| 70 | + packagedArtifactOwner: ObjectStackProtocolImplementation['packagedArtifactOwner']; |
| 71 | + packagedBaseRefusal: ObjectStackProtocolImplementation['packagedBaseRefusal']; |
| 72 | +} |
| 73 | + |
| 74 | +/** The body of `ObjectStackProtocolImplementation#servedLockState`; the method's docblock there is its contract. */ |
| 75 | +export function servedLockState( |
| 76 | + this: Pick<LockStateHost, 'packagedBaseRefusal'>, |
| 77 | + type: string, |
| 78 | + name: string, |
| 79 | + document: unknown, |
| 80 | + artifactBacked: boolean, |
| 81 | + itemLock: ItemLock, |
| 82 | + storedRowServed = false, |
| 83 | +): ReturnType<typeof resolveLockState> { |
| 84 | + const { provenance, packageId, packageVersion } = extractProtection(document); |
| 85 | + const editable = evaluateLockForWrite(itemLock.lock) === null |
| 86 | + && this.packagedBaseRefusal({ type, name, operation: 'save' }) === null; |
| 87 | + const removalRefusal = this.packagedBaseRefusal({ type, name, operation: 'delete' }); |
| 88 | + const deletable = evaluateLockForDelete(itemLock.lock) === null |
| 89 | + && (removalRefusal === null || (storedRowServed && isOriginGatedType(type))); |
| 90 | + const lock = MetadataLockSchema.options.find((state) => |
| 91 | + (evaluateLockForWrite(state) === null) === editable |
| 92 | + && (evaluateLockForDelete(state) === null) === deletable); |
| 93 | + if (lock === undefined) { |
| 94 | + // Unreachable while the lock algebra covers all four verdict pairs; |
| 95 | + // a state added to it without a write/delete answer must fail here, |
| 96 | + // loudly, not publish a guessed lock. |
| 97 | + throw new Error( |
| 98 | + `No ADR-0010 lock state answers editable=${editable}, deletable=${deletable}.`, |
| 99 | + ); |
| 100 | + } |
| 101 | + return { |
| 102 | + lock, |
| 103 | + lockReason: itemLock.lockReason, |
| 104 | + lockSource: itemLock.lockSource, |
| 105 | + lockDocsUrl: itemLock.lockDocsUrl, |
| 106 | + provenance, |
| 107 | + packageId, |
| 108 | + packageVersion, |
| 109 | + editable, |
| 110 | + deletable, |
| 111 | + resettable: artifactBacked, |
| 112 | + }; |
| 113 | +} |
| 114 | + |
| 115 | +/** The body of `ObjectStackProtocolImplementation#isShippedFlowName`; the method's docblock there is its contract. */ |
| 116 | +export function isShippedFlowName( |
| 117 | + this: Pick<LockStateHost, 'packagedArtifactOwner'>, |
| 118 | + type: string, name: unknown): boolean { |
| 119 | + if ((PLURAL_TO_SINGULAR[type] ?? type) !== 'flow') return false; |
| 120 | + if (typeof name !== 'string' || name === '') return false; |
| 121 | + return this.packagedArtifactOwner({ type: 'flow', name }) !== undefined; |
| 122 | +} |
| 123 | + |
| 124 | +/** The body of `ObjectStackProtocolImplementation#isStoredFlowEntryOfShippedName`; the method's docblock there is its contract. */ |
| 125 | +export function isStoredFlowEntryOfShippedName( |
| 126 | + this: Pick<LockStateHost, 'isShippedFlowName'>, |
| 127 | + type: string, item: unknown): boolean { |
| 128 | + return this.isShippedFlowName(type, (item as { name?: unknown } | null | undefined)?.name) |
| 129 | + && !isCodeArtifactBody(item); |
| 130 | +} |
| 131 | + |
| 132 | +/** The body of `ObjectStackProtocolImplementation#declinesStoredRow`; the method's docblock there is its contract. */ |
| 133 | +export function declinesStoredRow( |
| 134 | + this: Pick<LockStateHost, 'isDeclaredCodeDatasource' | 'isShippedFlowName' | 'isUnservedSealedOverlay'>, |
| 135 | + type: string, name: unknown): boolean { |
| 136 | + if (this.isShippedFlowName(type, name)) return true; |
| 137 | + if (typeof name !== 'string' || name === '') return false; |
| 138 | + return this.isDeclaredCodeDatasource(type, name) || this.isUnservedSealedOverlay(type, name); |
| 139 | +} |
| 140 | + |
| 141 | +/** The body of `ObjectStackProtocolImplementation#isUnservedSealedOverlay`; the method's docblock there is its contract. */ |
| 142 | +export function isUnservedSealedOverlay( |
| 143 | + this: Pick<LockStateHost, 'isSealedManagedItem'>, |
| 144 | + type: string, name: string): boolean { |
| 145 | + if (canonicalMetaType(type) === 'permission') return false; |
| 146 | + return this.isSealedManagedItem(type, name); |
| 147 | +} |
| 148 | + |
| 149 | +/** The body of `ObjectStackProtocolImplementation#isStoredEntryOfDeclinedName`; the method's docblock there is its contract. */ |
| 150 | +export function isStoredEntryOfDeclinedName( |
| 151 | + this: Pick<LockStateHost, 'declinesStoredRow'>, |
| 152 | + type: string, item: unknown): boolean { |
| 153 | + return this.declinesStoredRow(type, (item as { name?: unknown } | null | undefined)?.name) |
| 154 | + && !isCodeArtifactBody(item); |
| 155 | +} |
0 commit comments