|
1 | 1 | // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. |
2 | 2 |
|
3 | | -import { ServiceObject, ObjectSchema, ObjectOwnership, provisionPrimary, resolveCrudAffordances, isTenancyDisabled } from '@objectstack/spec/data'; |
| 3 | +import { ServiceObject, ObjectSchema, ObjectOwnership, provisionPrimary, resolveCrudAffordances, isTenancyDisabled, LEGACY_API_METHODS } from '@objectstack/spec/data'; |
4 | 4 | import { resolveMultiOrgEnabled, resolveSearchPinyinEnabled } from '@objectstack/types'; |
5 | 5 | import { provisionSearchCompanion } from './search-companion.js'; |
6 | 6 | import { ObjectStackManifest, ManifestSchema, InstalledPackage, InstalledPackageSchema } from '@objectstack/spec/kernel'; |
@@ -389,6 +389,16 @@ export function applySystemFields( |
389 | 389 | * Generic-write `apiMethods` verbs mapped to the {@link resolveCrudAffordances} |
390 | 390 | * flag each one needs. Read verbs (`get`/`list`/`search`/`history`/…) are |
391 | 391 | * always permitted, so they are absent here and never stripped. |
| 392 | + * |
| 393 | + * ⚠️ Two orthogonal axes — do NOT merge this with the API-tightening table. |
| 394 | + * This table (verb → *affordance*) is the UI-intent axis: it strips write verbs |
| 395 | + * a managed bucket does not *offer* from the whitelist. The verb → *primitive* |
| 396 | + * derivation that decides what the automatic API *admits* lives in |
| 397 | + * `@objectstack/spec/data` `API_METHOD_DERIVATION` / `resolveEffectiveApiMethods` |
| 398 | + * (#3391). The identical-shaped `WRITE_OP_AFFORDANCE` in plugin-security |
| 399 | + * `system-write-guard.ts` is the runtime enforcement of this same UI-intent |
| 400 | + * axis. Unifying the three is deferred to the enum-shrink (P2 of #3391); until |
| 401 | + * then keep the axes separate — see ADR-0103. |
392 | 402 | */ |
393 | 403 | const MANAGED_WRITE_VERB_AFFORDANCE: Record<string, 'create' | 'edit' | 'delete'> = { |
394 | 404 | create: 'create', |
@@ -463,6 +473,43 @@ export function reconcileManagedApiMethods( |
463 | 473 | }; |
464 | 474 | } |
465 | 475 |
|
| 476 | +/** Objects already warned about explicit legacy `apiMethods` (once per object). */ |
| 477 | +const warnedDeprecatedApiMethods = new Set<string>(); |
| 478 | + |
| 479 | +/** |
| 480 | + * Registration-time deprecation warning for standalone LEGACY `apiMethods` |
| 481 | + * values (#3391). The 8 legacy verbs (`upsert`/`aggregate`/`history`/`search`/ |
| 482 | + * `restore`/`purge`/`import`/`export`) are DERIVED from the six primitives; an |
| 483 | + * object that declares one explicitly is honored verbatim for one release |
| 484 | + * ("explicit wins"), then the value is removed in the enum-shrink (P2 of #3391). |
| 485 | + * |
| 486 | + * Emitted once per object (not per request — the hot path stays free), mirroring |
| 487 | + * the {@link reconcileManagedApiMethods} warning format and pointing at P2. Pure |
| 488 | + * observation: it never mutates the schema. |
| 489 | + */ |
| 490 | +export function warnDeprecatedExplicitApiMethods( |
| 491 | + schema: ServiceObject, |
| 492 | + opts?: { warn?: (msg: string) => void }, |
| 493 | +): void { |
| 494 | + const methods = (schema as any).enable?.apiMethods; |
| 495 | + if (!Array.isArray(methods) || methods.length === 0) return; |
| 496 | + const legacy = methods.filter((m: string) => (LEGACY_API_METHODS as readonly string[]).includes(m)); |
| 497 | + if (legacy.length === 0) return; |
| 498 | + const name = String((schema as any).name ?? ''); |
| 499 | + if (warnedDeprecatedApiMethods.has(name)) return; |
| 500 | + warnedDeprecatedApiMethods.add(name); |
| 501 | + const warn = opts?.warn ?? ((msg: string) => console.warn(msg)); |
| 502 | + warn( |
| 503 | + `[Registry] Object "${name}" declares derived legacy apiMethods value(s) ` + |
| 504 | + `[${legacy.join(', ')}] in enable.apiMethods — these derive from the six ` + |
| 505 | + `primitives (get/list/create/update/delete/bulk) and are honored verbatim ` + |
| 506 | + `for now, but standalone declaration is deprecated and will be removed in ` + |
| 507 | + `the enum-shrink (P2 of #3391). Declare the underlying primitives instead ` + |
| 508 | + `(e.g. ['create','update'] grants upsert/import; ['list'] grants ` + |
| 509 | + `aggregate/export/search).`, |
| 510 | + ); |
| 511 | +} |
| 512 | + |
466 | 513 | /** |
467 | 514 | * Platform namespaces that multiple packages may legitimately share, so the |
468 | 515 | * install-time namespace-uniqueness gate (ADR-0048 Phase 1) must never fire on |
@@ -714,6 +761,11 @@ export class SchemaRegistry { |
714 | 761 | // every non-`better-auth` object. |
715 | 762 | schema = reconcileManagedApiMethods(schema); |
716 | 763 |
|
| 764 | + // [#3391] One-shot deprecation warning for standalone LEGACY apiMethods |
| 765 | + // values (the derived 8) — honored this release, removed in the enum-shrink. |
| 766 | + // Runs after reconcile so we warn about what actually ships. |
| 767 | + warnDeprecatedExplicitApiMethods(schema); |
| 768 | + |
717 | 769 | // [ADR-0079] Object-materialization seam — DESIGNATE-ONLY primary-title |
718 | 770 | // provisioning. Runs AFTER `applySystemFields` (so any designated field |
719 | 771 | // co-exists with the injected system columns) and ONLY for owned objects |
|
0 commit comments