diff --git a/.changeset/remove-enable-trash-mru.md b/.changeset/remove-enable-trash-mru.md new file mode 100644 index 0000000000..addb55f249 --- /dev/null +++ b/.changeset/remove-enable-trash-mru.md @@ -0,0 +1,33 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec)!: remove the dead `object.enable.trash` / `enable.mru` capability flags (#2377, ADR-0049 enforce-or-remove — close-out) + +Both flags parsed and defaulted to `true` but had **no runtime consumer**: +every delete has always been a hard delete (no recycle bin), and no MRU +tracking was ever implemented. A default-true flag promising recoverability +is the worst kind of false affordance — first-party objects were authoring +`trash: false // Never soft-delete audit logs` in the belief that a +soft-delete existed to opt out of. + +- `ObjectCapabilities` is now **`.strict()`** (pattern of the tenancy block, + #2763): an unknown `enable` key — the retired `trash`/`mru` or a typo like + `feedEnabled` — fails parse with upgrade guidance instead of stripping + silently (#1535). The retired-key tombstones live in + `CAPABILITIES_RETIRED_KEY_GUIDANCE`. +- ~45 first-party object definitions (platform-objects, plugin-security, + plugin-audit, plugin-approvals, plugin-sharing, metadata-core, + service-realtime, examples) dropped their inert `trash:`/`mru:` lines. +- Liveness ledger: both entries deleted (removal precedent: `tags`/ + `recordName`); object row in the README count table now shows **0 dead**. +- Docs + skills no longer advertise a recycle bin / MRU tracking; the API + skill's "DELETE is soft-delete when `trash: true`" claim is corrected to + the real contract (hard delete; use per-field `trackHistory` or a + `lifecycle` policy for recoverability). + +**Migration**: delete any `enable.trash` / `enable.mru` keys from object +metadata — they never changed behavior. `ObjectSchema.create()` / +`ObjectCapabilities.parse()` now reject them with this prescription. A real +recycle bin or MRU feature, if built, returns as a live enforced flag +(#1893 prune-or-build). diff --git a/content/docs/concepts/metadata-driven.mdx b/content/docs/concepts/metadata-driven.mdx index 85e852d6a0..ebd121bd1b 100644 --- a/content/docs/concepts/metadata-driven.mdx +++ b/content/docs/concepts/metadata-driven.mdx @@ -470,8 +470,6 @@ export const Account = ObjectSchema.create({ files: true, // Enable file attachments feeds: true, // Enable activity feed activities: true, // Enable tasks and events - trash: true, // Enable soft delete - mru: true, // Track Most Recently Used }, }); ``` diff --git a/content/docs/data-modeling/objects.mdx b/content/docs/data-modeling/objects.mdx index 61d4bba085..c52c53c3a3 100644 --- a/content/docs/data-modeling/objects.mdx +++ b/content/docs/data-modeling/objects.mdx @@ -83,8 +83,6 @@ enable: { files: true, // File attachments feeds: true, // Activity feed and comments activities: true, // Tasks and events tracking - trash: true, // Soft delete with restore - mru: true, // Most Recently Used tracking clone: true, // Deep record cloning } ``` @@ -98,8 +96,6 @@ enable: { | `files` | `false` | Enable file attachments | | `feeds` | `true` | Enable social feed and comments | | `activities` | `true` | Enable tasks and events tracking | -| `trash` | `true` | Enable soft delete with restore | -| `mru` | `true` | Track Most Recently Used list | | `clone` | `true` | Allow record deep cloning | ### Enterprise Features @@ -330,7 +326,6 @@ export const ProjectTask = ObjectSchema.create({ searchable: true, trackHistory: true, feeds: true, - trash: true, }, validations: [ diff --git a/content/docs/data-modeling/schema-design.mdx b/content/docs/data-modeling/schema-design.mdx index 93b6b7cec1..de9fe2d69d 100644 --- a/content/docs/data-modeling/schema-design.mdx +++ b/content/docs/data-modeling/schema-design.mdx @@ -80,8 +80,6 @@ enable: { files: true, // Allow file attachments feeds: true, // Enable activity feed (Chatter-like) activities: true, // Track tasks and events - trash: true, // Soft delete with recycle bin - mru: true, // Track Most Recently Used } ``` diff --git a/content/docs/protocol/objectql/schema.mdx b/content/docs/protocol/objectql/schema.mdx index b1ae119412..3f79bc7544 100644 --- a/content/docs/protocol/objectql/schema.mdx +++ b/content/docs/protocol/objectql/schema.mdx @@ -235,8 +235,6 @@ enable: # Data Management trackHistory: true # Track field history (who changed what, when) searchable: true # Index for global search - trash: true # Enable soft-delete with restore capability - # API & Integration apiEnabled: true # Generate REST/GraphQL endpoints @@ -246,7 +244,6 @@ enable: activities: true # Enable tasks and events # User Experience - mru: true # Track Most Recently Used list clone: true # Allow record deep cloning ``` diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index 93b11cfd62..dc93561ab4 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -164,8 +164,6 @@ const result = ApiMethod.parse(data); | **files** | `boolean` | ✅ | Generic record Attachments panel (sys_attachment). Opt-in: true surfaces the panel and permits attachments targeting this object; otherwise creation is rejected. Field.file/Field.image are independent | | **feeds** | `boolean` | ✅ | Record comments/collaboration feed. Default on; explicit false hides the feed UI and rejects new comments for this object | | **activities** | `boolean` | ✅ | Record activity timeline (sys_activity mirror of CRUD). Default on; explicit false stops mirroring and hides the timeline | -| **trash** | `boolean` | ✅ | Enable soft-delete with restore capability | -| **mru** | `boolean` | ✅ | Track Most Recently Used (MRU) list for users | | **clone** | `boolean` | ✅ | Allow record deep cloning | diff --git a/examples/app-todo/src/objects/task.object.ts b/examples/app-todo/src/objects/task.object.ts index d5d686655a..cdc9d39e35 100644 --- a/examples/app-todo/src/objects/task.object.ts +++ b/examples/app-todo/src/objects/task.object.ts @@ -171,8 +171,6 @@ export const Task = ObjectSchema.create({ files: true, feeds: true, activities: true, - trash: true, - mru: true, }, // Database indexes for performance diff --git a/packages/cli/src/utils/lint-liveness-properties.test.ts b/packages/cli/src/utils/lint-liveness-properties.test.ts index b7704b8b6b..38afbb0e41 100644 --- a/packages/cli/src/utils/lint-liveness-properties.test.ts +++ b/packages/cli/src/utils/lint-liveness-properties.test.ts @@ -22,9 +22,9 @@ describe('lintLivenessProperties', () => { // action.undoable). The object/field WALKER is still exercised by the // silent-clean and default-on-suppression cases below. - it('does NOT warn on a default-on flag the author left alone (enable.trash: true)', () => { - const findings = lintLivenessProperties(objStack({ enable: { trash: true } })); - expect(paths(findings).some((m) => m.includes('enable.trash'))).toBe(false); + it('does NOT warn on a default-on flag the author left alone (enable.searchable: true)', () => { + const findings = lintLivenessProperties(objStack({ enable: { searchable: true } })); + expect(paths(findings).some((m) => m.includes('enable.searchable'))).toBe(false); }); // #2707/#2727: every ObjectCapabilities flag is now LIVE (opt-out diff --git a/packages/cli/src/utils/lint-liveness-properties.ts b/packages/cli/src/utils/lint-liveness-properties.ts index 3d1dcdcf41..99abbf197a 100644 --- a/packages/cli/src/utils/lint-liveness-properties.ts +++ b/packages/cli/src/utils/lint-liveness-properties.ts @@ -16,7 +16,7 @@ * `"authorWarn": true` (+ an optional `"authorHint"`). A property being merely * `dead` is NOT enough — plenty of dead props are benign display/doc metadata. * Only entries an author would be *misled* by are marked. Booleans warn only when - * set truthy (so schema defaults like `enable.trash` never trip it); object/ + * set truthy (so schema defaults like `enable.searchable` never trip it); object/ * string/array props warn when present at all. */ diff --git a/packages/metadata-core/src/objects/sys-metadata-audit.object.ts b/packages/metadata-core/src/objects/sys-metadata-audit.object.ts index 115ec732be..292e80c847 100644 --- a/packages/metadata-core/src/objects/sys-metadata-audit.object.ts +++ b/packages/metadata-core/src/objects/sys-metadata-audit.object.ts @@ -180,6 +180,5 @@ export const SysMetadataAuditObject = ObjectSchema.create({ searchable: false, apiEnabled: true, apiMethods: ['get', 'list'], - trash: false, }, }); diff --git a/packages/metadata-core/src/objects/sys-metadata-commit.object.ts b/packages/metadata-core/src/objects/sys-metadata-commit.object.ts index f838b30529..7781fe47cf 100644 --- a/packages/metadata-core/src/objects/sys-metadata-commit.object.ts +++ b/packages/metadata-core/src/objects/sys-metadata-commit.object.ts @@ -148,6 +148,5 @@ export const SysMetadataCommitObject = ObjectSchema.create({ searchable: false, apiEnabled: true, apiMethods: ['get', 'list'], - trash: false, }, }); diff --git a/packages/metadata-core/src/objects/sys-metadata-history.object.ts b/packages/metadata-core/src/objects/sys-metadata-history.object.ts index eeeac2db5f..e1ae807fde 100644 --- a/packages/metadata-core/src/objects/sys-metadata-history.object.ts +++ b/packages/metadata-core/src/objects/sys-metadata-history.object.ts @@ -184,6 +184,5 @@ export const SysMetadataHistoryObject = ObjectSchema.create({ searchable: false, apiEnabled: true, apiMethods: ['get', 'list'], - trash: false, }, }); diff --git a/packages/metadata-core/src/objects/sys-metadata.object.ts b/packages/metadata-core/src/objects/sys-metadata.object.ts index 96bb2f3a9c..0281d79f7b 100644 --- a/packages/metadata-core/src/objects/sys-metadata.object.ts +++ b/packages/metadata-core/src/objects/sys-metadata.object.ts @@ -234,7 +234,6 @@ export const SysMetadataObject = ObjectSchema.create({ // so the generic write verbs were a latent hole for a user-context raw write // the bucket forbids. Reads stay open for the Setup "Data Model" grids. apiMethods: ['get', 'list'], - trash: false, }, // Named list views — power the Setup App "Data Model" group so admins diff --git a/packages/metadata-core/src/objects/sys-view-definition.object.ts b/packages/metadata-core/src/objects/sys-view-definition.object.ts index 8f3637fad8..8884da0387 100644 --- a/packages/metadata-core/src/objects/sys-view-definition.object.ts +++ b/packages/metadata-core/src/objects/sys-view-definition.object.ts @@ -139,6 +139,5 @@ export const SysViewDefinitionObject = ObjectSchema.create({ searchable: false, apiEnabled: true, apiMethods: ['get', 'list', 'create', 'update', 'delete'], - trash: false, }, }); diff --git a/packages/platform-objects/src/audit/sys-attachment.object.ts b/packages/platform-objects/src/audit/sys-attachment.object.ts index 6490481126..b86b64f4e5 100644 --- a/packages/platform-objects/src/audit/sys-attachment.object.ts +++ b/packages/platform-objects/src/audit/sys-attachment.object.ts @@ -137,8 +137,6 @@ export const SysAttachment = ObjectSchema.create({ // reap guard reclaims a file's bytes once its last join row is gone, so a // "restore" would dangle. Declare `false` — the honest state — rather than // claim a restore capability the runtime does not provide. - trash: false, - mru: false, clone: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-account.object.ts b/packages/platform-objects/src/identity/sys-account.object.ts index c27eca6b23..9ba9bb3868 100644 --- a/packages/platform-objects/src/identity/sys-account.object.ts +++ b/packages/platform-objects/src/identity/sys-account.object.ts @@ -217,7 +217,5 @@ export const SysAccount = ObjectSchema.create({ // #1591 — reads only: writes are refused by the identity write guard // (ADR-0092 D2) and owned by better-auth. HTTP answers 405 before the 403. apiMethods: ['get', 'list'], - trash: true, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-api-key.object.ts b/packages/platform-objects/src/identity/sys-api-key.object.ts index e99972a598..66cf1d5270 100644 --- a/packages/platform-objects/src/identity/sys-api-key.object.ts +++ b/packages/platform-objects/src/identity/sys-api-key.object.ts @@ -217,7 +217,5 @@ export const SysApiKey = ObjectSchema.create({ // #1591 — reads only: writes are refused by the identity write guard // (ADR-0092 D2) and owned by better-auth. HTTP answers 405 before the 403. apiMethods: ['get', 'list'], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-business-unit-member.object.ts b/packages/platform-objects/src/identity/sys-business-unit-member.object.ts index 4ee73d2e2f..15493dac4c 100644 --- a/packages/platform-objects/src/identity/sys-business-unit-member.object.ts +++ b/packages/platform-objects/src/identity/sys-business-unit-member.object.ts @@ -109,7 +109,5 @@ export const SysBusinessUnitMember = ObjectSchema.create({ // entries. Reconcile-safe: import/export are not generic write verbs, so // reconcileManagedApiMethods (managedBy:'platform') never strips them. apiMethods: ['get', 'list', 'create', 'update', 'delete', 'import', 'export'], - trash: true, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-business-unit.object.ts b/packages/platform-objects/src/identity/sys-business-unit.object.ts index bc9cbc1292..35a6d47db6 100644 --- a/packages/platform-objects/src/identity/sys-business-unit.object.ts +++ b/packages/platform-objects/src/identity/sys-business-unit.object.ts @@ -220,7 +220,5 @@ export const SysBusinessUnit = ObjectSchema.create({ // and `effective_from/to` are designed for HRIS batch sync (#3025). Import // reuses the create/update affordances this object already grants. apiMethods: ['get', 'list', 'create', 'update', 'delete', 'import', 'export'], - trash: true, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-device-code.object.ts b/packages/platform-objects/src/identity/sys-device-code.object.ts index 9a9d78634a..563eaa1a55 100644 --- a/packages/platform-objects/src/identity/sys-device-code.object.ts +++ b/packages/platform-objects/src/identity/sys-device-code.object.ts @@ -142,7 +142,5 @@ export const SysDeviceCode = ObjectSchema.create({ // #1591 — reads only: writes are refused by the identity write guard // (ADR-0092 D2) and owned by better-auth. HTTP answers 405 before the 403. apiMethods: ['get'], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-invitation.object.ts b/packages/platform-objects/src/identity/sys-invitation.object.ts index ddf4553c46..c08d033fa9 100644 --- a/packages/platform-objects/src/identity/sys-invitation.object.ts +++ b/packages/platform-objects/src/identity/sys-invitation.object.ts @@ -245,7 +245,5 @@ export const SysInvitation = ObjectSchema.create({ // #1591 — reads only: writes are refused by the identity write guard // (ADR-0092 D2) and owned by better-auth. HTTP answers 405 before the 403. apiMethods: ['get', 'list'], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-jwks.object.ts b/packages/platform-objects/src/identity/sys-jwks.object.ts index 3c7fb319d1..be634405d4 100644 --- a/packages/platform-objects/src/identity/sys-jwks.object.ts +++ b/packages/platform-objects/src/identity/sys-jwks.object.ts @@ -95,7 +95,5 @@ export const SysJwks = ObjectSchema.create({ searchable: false, apiEnabled: false, apiMethods: [], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-member.object.ts b/packages/platform-objects/src/identity/sys-member.object.ts index e3c9155ded..53dbbd88eb 100644 --- a/packages/platform-objects/src/identity/sys-member.object.ts +++ b/packages/platform-objects/src/identity/sys-member.object.ts @@ -190,7 +190,5 @@ export const SysMember = ObjectSchema.create({ // #1591 — reads only: writes are refused by the identity write guard // (ADR-0092 D2) and owned by better-auth. HTTP answers 405 before the 403. apiMethods: ['get', 'list'], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-oauth-access-token.object.ts b/packages/platform-objects/src/identity/sys-oauth-access-token.object.ts index d2e0708adf..0b06b8cf3f 100644 --- a/packages/platform-objects/src/identity/sys-oauth-access-token.object.ts +++ b/packages/platform-objects/src/identity/sys-oauth-access-token.object.ts @@ -146,7 +146,5 @@ export const SysOauthAccessToken = ObjectSchema.create({ searchable: false, apiEnabled: false, apiMethods: [], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-oauth-application.object.ts b/packages/platform-objects/src/identity/sys-oauth-application.object.ts index 883c816d8b..c4a1efe97c 100644 --- a/packages/platform-objects/src/identity/sys-oauth-application.object.ts +++ b/packages/platform-objects/src/identity/sys-oauth-application.object.ts @@ -491,7 +491,5 @@ export const SysOauthApplication = ObjectSchema.create({ // bypass server-side OAuth validation. The Delete row action above is // wired to /api/v1/auth/oauth2/delete-client. apiMethods: ['get', 'list'], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-oauth-client-assertion.object.ts b/packages/platform-objects/src/identity/sys-oauth-client-assertion.object.ts index 0bacc1c0e7..db7aeb991f 100644 --- a/packages/platform-objects/src/identity/sys-oauth-client-assertion.object.ts +++ b/packages/platform-objects/src/identity/sys-oauth-client-assertion.object.ts @@ -49,7 +49,5 @@ export const SysOauthClientAssertion = ObjectSchema.create({ searchable: false, apiEnabled: false, apiMethods: [], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-oauth-client-resource.object.ts b/packages/platform-objects/src/identity/sys-oauth-client-resource.object.ts index c3e869fa08..2a6c85ee0d 100644 --- a/packages/platform-objects/src/identity/sys-oauth-client-resource.object.ts +++ b/packages/platform-objects/src/identity/sys-oauth-client-resource.object.ts @@ -73,7 +73,5 @@ export const SysOauthClientResource = ObjectSchema.create({ searchable: false, apiEnabled: false, apiMethods: [], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-oauth-consent.object.ts b/packages/platform-objects/src/identity/sys-oauth-consent.object.ts index ab8d786b40..f236f35cc4 100644 --- a/packages/platform-objects/src/identity/sys-oauth-consent.object.ts +++ b/packages/platform-objects/src/identity/sys-oauth-consent.object.ts @@ -101,7 +101,5 @@ export const SysOauthConsent = ObjectSchema.create({ searchable: false, apiEnabled: false, apiMethods: [], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-oauth-refresh-token.object.ts b/packages/platform-objects/src/identity/sys-oauth-refresh-token.object.ts index 4e38f1d51f..f7d14d9bfc 100644 --- a/packages/platform-objects/src/identity/sys-oauth-refresh-token.object.ts +++ b/packages/platform-objects/src/identity/sys-oauth-refresh-token.object.ts @@ -162,7 +162,5 @@ export const SysOauthRefreshToken = ObjectSchema.create({ searchable: false, apiEnabled: false, apiMethods: [], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-oauth-resource.object.ts b/packages/platform-objects/src/identity/sys-oauth-resource.object.ts index 8626a8665a..bdc4513196 100644 --- a/packages/platform-objects/src/identity/sys-oauth-resource.object.ts +++ b/packages/platform-objects/src/identity/sys-oauth-resource.object.ts @@ -139,7 +139,5 @@ export const SysOauthResource = ObjectSchema.create({ searchable: false, apiEnabled: false, apiMethods: [], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-organization.object.ts b/packages/platform-objects/src/identity/sys-organization.object.ts index 68c42ad4ad..91b1628469 100644 --- a/packages/platform-objects/src/identity/sys-organization.object.ts +++ b/packages/platform-objects/src/identity/sys-organization.object.ts @@ -250,7 +250,5 @@ export const SysOrganization = ObjectSchema.create({ // #1591 — reads only: writes are refused by the identity write guard // (ADR-0092 D2) and owned by better-auth. HTTP answers 405 before the 403. apiMethods: ['get', 'list'], - trash: true, - mru: true, }, }); diff --git a/packages/platform-objects/src/identity/sys-scim-provider.object.ts b/packages/platform-objects/src/identity/sys-scim-provider.object.ts index ded4a1f2bd..8bddcbe422 100644 --- a/packages/platform-objects/src/identity/sys-scim-provider.object.ts +++ b/packages/platform-objects/src/identity/sys-scim-provider.object.ts @@ -120,7 +120,5 @@ export const SysScimProvider = ObjectSchema.create({ // under /api/v1/auth/scim/*; the generic data layer is read-only so the // credential cannot be written/bypassed through it. apiMethods: ['list'], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-session.object.ts b/packages/platform-objects/src/identity/sys-session.object.ts index 2eb41c8f1e..51846b03ed 100644 --- a/packages/platform-objects/src/identity/sys-session.object.ts +++ b/packages/platform-objects/src/identity/sys-session.object.ts @@ -213,8 +213,6 @@ export const SysSession = ObjectSchema.create({ // #1591 — reads only: writes are refused by the identity write guard // (ADR-0092 D2) and owned by better-auth. HTTP answers 405 before the 403. apiMethods: ['get', 'list'], - trash: false, - mru: false, clone: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-sso-provider.object.ts b/packages/platform-objects/src/identity/sys-sso-provider.object.ts index 4f90ee0536..27f923c7bb 100644 --- a/packages/platform-objects/src/identity/sys-sso-provider.object.ts +++ b/packages/platform-objects/src/identity/sys-sso-provider.object.ts @@ -297,7 +297,5 @@ export const SysSsoProvider = ObjectSchema.create({ // the generic data layer is read-only so sysadmins cannot bypass // server-side validation / secret handling. apiMethods: ['get', 'list'], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-team-member.object.ts b/packages/platform-objects/src/identity/sys-team-member.object.ts index 2c0da7e7d8..c3f55351aa 100644 --- a/packages/platform-objects/src/identity/sys-team-member.object.ts +++ b/packages/platform-objects/src/identity/sys-team-member.object.ts @@ -113,7 +113,5 @@ export const SysTeamMember = ObjectSchema.create({ // #1591 — reads only: writes are refused by the identity write guard // (ADR-0092 D2) and owned by better-auth. HTTP answers 405 before the 403. apiMethods: ['get', 'list'], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-team.object.ts b/packages/platform-objects/src/identity/sys-team.object.ts index 7eecf4f82b..0e4f991da8 100644 --- a/packages/platform-objects/src/identity/sys-team.object.ts +++ b/packages/platform-objects/src/identity/sys-team.object.ts @@ -177,7 +177,5 @@ export const SysTeam = ObjectSchema.create({ // The HTTP layer now answers 405 (api-exposure) before the engine's // 403 backstop. See #1591. apiMethods: ['get', 'list'], - trash: true, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-two-factor.object.ts b/packages/platform-objects/src/identity/sys-two-factor.object.ts index 0db40363b6..2115959b18 100644 --- a/packages/platform-objects/src/identity/sys-two-factor.object.ts +++ b/packages/platform-objects/src/identity/sys-two-factor.object.ts @@ -177,7 +177,5 @@ export const SysTwoFactor = ObjectSchema.create({ // #1591 — reads only: writes are refused by the identity write guard // (ADR-0092 D2) and owned by better-auth. HTTP answers 405 before the 403. apiMethods: ['get'], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-user-preference.object.ts b/packages/platform-objects/src/identity/sys-user-preference.object.ts index 2da965fe20..dfe77d8788 100644 --- a/packages/platform-objects/src/identity/sys-user-preference.object.ts +++ b/packages/platform-objects/src/identity/sys-user-preference.object.ts @@ -116,7 +116,5 @@ export const SysUserPreference = ObjectSchema.create({ searchable: false, apiEnabled: true, apiMethods: ['get', 'list', 'create', 'update', 'delete'], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/identity/sys-user.object.ts b/packages/platform-objects/src/identity/sys-user.object.ts index c6f75d4239..a6a4ea06ce 100644 --- a/packages/platform-objects/src/identity/sys-user.object.ts +++ b/packages/platform-objects/src/identity/sys-user.object.ts @@ -760,8 +760,6 @@ export const SysUser = ObjectSchema.create({ // clamped to the profile-field whitelist ({name, image}) by the guard — // `userActions.edit: true` above declares the affordance. apiMethods: ['get', 'list', 'update'], - trash: true, - mru: true, }, // Email uniqueness is enforced by the unique index above (and better-auth's diff --git a/packages/platform-objects/src/identity/sys-verification.object.ts b/packages/platform-objects/src/identity/sys-verification.object.ts index 74e29bb570..06fd2ddd97 100644 --- a/packages/platform-objects/src/identity/sys-verification.object.ts +++ b/packages/platform-objects/src/identity/sys-verification.object.ts @@ -91,7 +91,5 @@ export const SysVerification = ObjectSchema.create({ // #1591 — reads only: writes are refused by the identity write guard // (ADR-0092 D2) and owned by better-auth. HTTP answers 405 before the 403. apiMethods: ['get'], - trash: false, - mru: false, }, }); diff --git a/packages/platform-objects/src/system/sys-secret.object.ts b/packages/platform-objects/src/system/sys-secret.object.ts index e289d9b608..9ca02791f7 100644 --- a/packages/platform-objects/src/system/sys-secret.object.ts +++ b/packages/platform-objects/src/system/sys-secret.object.ts @@ -136,7 +136,6 @@ export const SysSecret = ObjectSchema.create({ enable: { trackHistory: false, // rotation events are recorded by sys_setting_audit - audit: true, // [ADR-0103] Engine-owned: secrets are minted/rotated only by the settings / // secret service (SYSTEM_CTX), never via the generic data API. Locked to // reads (ciphertext only; decryption is a separate privileged path) — an diff --git a/packages/platform-objects/src/system/sys-setting-audit.object.ts b/packages/platform-objects/src/system/sys-setting-audit.object.ts index 8daacfef14..c2f9836761 100644 --- a/packages/platform-objects/src/system/sys-setting-audit.object.ts +++ b/packages/platform-objects/src/system/sys-setting-audit.object.ts @@ -174,7 +174,6 @@ export const SysSettingAudit = ObjectSchema.create({ enable: { trackHistory: false, - audit: false, // this IS the audit; no recursion // [ADR-0103] Engine-owned: appended by the settings service (SYSTEM_CTX), // never via the generic data API. Reads stay open for compliance review. apiMethods: ['get', 'list'], diff --git a/packages/platform-objects/src/system/sys-setting.object.ts b/packages/platform-objects/src/system/sys-setting.object.ts index 4959de7767..29fe512255 100644 --- a/packages/platform-objects/src/system/sys-setting.object.ts +++ b/packages/platform-objects/src/system/sys-setting.object.ts @@ -198,7 +198,5 @@ export const SysSetting = ObjectSchema.create({ // the UI MUST go through /api/settings/:namespace so the resolver // and audit hooks fire. The grid is diagnostic-only. apiMethods: ['get', 'list'], - trash: false, - mru: false, }, }); diff --git a/packages/plugins/plugin-approvals/src/sys-approval-delegation.object.ts b/packages/plugins/plugin-approvals/src/sys-approval-delegation.object.ts index 2a5b112d70..e5671e4ad7 100644 --- a/packages/plugins/plugin-approvals/src/sys-approval-delegation.object.ts +++ b/packages/plugins/plugin-approvals/src/sys-approval-delegation.object.ts @@ -136,7 +136,5 @@ export const SysApprovalDelegation = ObjectSchema.create({ searchable: true, apiEnabled: true, apiMethods: ['get', 'list', 'create', 'update', 'delete'], - trash: true, - mru: false, }, }); diff --git a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts index fb956fa8bd..01e2e8dce7 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts @@ -208,8 +208,6 @@ export const SysActivity = ObjectSchema.create({ searchable: true, apiEnabled: true, apiMethods: ['get', 'list'], - trash: false, - mru: false, clone: false, }, }); diff --git a/packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts b/packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts index 2f5fc536b1..02527f95ae 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts @@ -225,8 +225,6 @@ export const SysAuditLog = ObjectSchema.create({ searchable: true, apiEnabled: true, apiMethods: ['get', 'list'], // Read-only — creation happens via internal system hooks only - trash: false, // Never soft-delete audit logs - mru: false, clone: false, }, }); diff --git a/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts b/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts index 28aaf25a54..847d361341 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts @@ -153,8 +153,6 @@ export const SysComment = ObjectSchema.create({ trackHistory: true, searchable: true, apiEnabled: true, - trash: true, - mru: false, clone: false, }, }); diff --git a/packages/plugins/plugin-security/src/objects/sys-audience-binding-suggestion.object.ts b/packages/plugins/plugin-security/src/objects/sys-audience-binding-suggestion.object.ts index aabb52fa8c..4732b4030e 100644 --- a/packages/plugins/plugin-security/src/objects/sys-audience-binding-suggestion.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-audience-binding-suggestion.object.ts @@ -117,7 +117,5 @@ export const SysAudienceBindingSuggestion = ObjectSchema.create({ // Read-only over the generic data API — confirm/dismiss go through the // `security` service so the anchor + delegated-admin gates always apply. apiMethods: ['get', 'list'], - trash: false, - mru: false, }, }); diff --git a/packages/plugins/plugin-security/src/objects/sys-capability.object.ts b/packages/plugins/plugin-security/src/objects/sys-capability.object.ts index 58d978b4ab..2eb54369a3 100644 --- a/packages/plugins/plugin-security/src/objects/sys-capability.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-capability.object.ts @@ -215,7 +215,5 @@ export const SysCapability = ObjectSchema.create({ searchable: true, apiEnabled: true, apiMethods: ['get', 'list', 'create', 'update', 'delete'], - trash: true, - mru: false, }, }); diff --git a/packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts b/packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts index 987be4e43d..35d5ca78ec 100644 --- a/packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts @@ -300,7 +300,5 @@ export const SysPermissionSet = ObjectSchema.create({ searchable: true, apiEnabled: true, apiMethods: ['get', 'list', 'create', 'update', 'delete'], - trash: true, - mru: true, }, }); diff --git a/packages/plugins/plugin-security/src/objects/sys-position-permission-set.object.ts b/packages/plugins/plugin-security/src/objects/sys-position-permission-set.object.ts index 3d40afe2f7..7acdc0168d 100644 --- a/packages/plugins/plugin-security/src/objects/sys-position-permission-set.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-position-permission-set.object.ts @@ -78,7 +78,5 @@ export const SysPositionPermissionSet = ObjectSchema.create({ searchable: true, apiEnabled: true, apiMethods: ['get', 'list', 'create', 'update', 'delete'], - trash: true, - mru: false, }, }); diff --git a/packages/plugins/plugin-security/src/objects/sys-position.object.ts b/packages/plugins/plugin-security/src/objects/sys-position.object.ts index db435dc7ce..ec03235db8 100644 --- a/packages/plugins/plugin-security/src/objects/sys-position.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-position.object.ts @@ -276,7 +276,5 @@ export const SysPosition = ObjectSchema.create({ searchable: true, apiEnabled: true, apiMethods: ['get', 'list', 'create', 'update', 'delete'], - trash: true, - mru: true, }, }); diff --git a/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts b/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts index 88ddf94ecb..ba59732836 100644 --- a/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts @@ -136,7 +136,5 @@ export const SysUserPermissionSet = ObjectSchema.create({ searchable: true, apiEnabled: true, apiMethods: ['get', 'list', 'create', 'update', 'delete'], - trash: true, - mru: false, }, }); diff --git a/packages/plugins/plugin-security/src/objects/sys-user-position.object.ts b/packages/plugins/plugin-security/src/objects/sys-user-position.object.ts index 4a1255496e..3b3de009fe 100644 --- a/packages/plugins/plugin-security/src/objects/sys-user-position.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-user-position.object.ts @@ -157,7 +157,5 @@ export const SysUserPosition = ObjectSchema.create({ searchable: true, apiEnabled: true, apiMethods: ['get', 'list', 'create', 'update', 'delete'], - trash: true, - mru: false, }, }); diff --git a/packages/plugins/plugin-sharing/src/objects/sys-share-link.object.ts b/packages/plugins/plugin-sharing/src/objects/sys-share-link.object.ts index dd3d475550..e1ed00fbf4 100644 --- a/packages/plugins/plugin-sharing/src/objects/sys-share-link.object.ts +++ b/packages/plugins/plugin-sharing/src/objects/sys-share-link.object.ts @@ -248,8 +248,6 @@ export const SysShareLink = ObjectSchema.create({ // The /api/v1/share-links endpoints are the authoritative surface; // the generic data API is exposed read-only for the admin grid. apiMethods: ['get', 'list'], - trash: false, - mru: false, clone: false, }, }); diff --git a/packages/services/service-realtime/src/objects/sys-presence.object.ts b/packages/services/service-realtime/src/objects/sys-presence.object.ts index 562961a00f..039ac421f3 100644 --- a/packages/services/service-realtime/src/objects/sys-presence.object.ts +++ b/packages/services/service-realtime/src/objects/sys-presence.object.ts @@ -120,7 +120,5 @@ export const SysPresence = ObjectSchema.create({ // /data route open for a user-context write the bucket forbids. Reads // stay open for diagnostic list views. apiMethods: ['get', 'list'], - trash: false, - mru: false, }, }); diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index a34cdcc880..c8b54743ff 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -110,7 +110,7 @@ Two rules keep it false-positive-free, **both of which the marker author must re marked; an author isn't misled by them. 2. **Booleans: only mark `default(false)` flags.** The lint warns on a boolean only when set `true`, and it can't tell author-set-`true` from a schema default. A `default(true)` flag - (`enable.trash`/`mru`, `enable.searchable`) would then warn on *every* object that has an + (`enable.searchable`) would then warn on *every* object that has an `enable` block — so leave those unmarked (see `enable.searchable`'s `_authorWarnSkipped`). Object/string/array props warn when merely present, so this caveat is boolean-only. @@ -189,7 +189,7 @@ EOF | Type | live | exp | dead | planned | Notes | |---|---|---|---|---|---| -| object | 40 | – | 2 | 1 | aspirational tier (versioning/softDelete/search/recordName/keyPrefix) + tags/active/abstract REMOVED (#2377) — tombstoned in UNKNOWN_KEY_GUIDANCE; `isSystem` + `enable.searchable` CORRECTED to live (#2377 — sharing default-model + global-search opt-out; 2026-06 audit missed both readers); remaining dead = `enable.trash`/`mru` (inert default-true, ~35 sys-object setters → deferred); `tenancy.strategy`/`crossTenantAccess` REMOVED post-15.0 (#2763) | +| object | 40 | – | 0 | 1 | aspirational tier (versioning/softDelete/search/recordName/keyPrefix) + tags/active/abstract REMOVED (#2377) — tombstoned in UNKNOWN_KEY_GUIDANCE; `enable.trash`/`mru` REMOVED (#2377 close-out) — tombstoned in the now-`.strict()` ObjectCapabilities; `isSystem` + `enable.searchable` CORRECTED to live (#2377 — sharing default-model + global-search opt-out; 2026-06 audit missed both readers); `tenancy.strategy`/`crossTenantAccess` REMOVED post-15.0 (#2763) | | field | 55 | – | 0 | – | healthy — full dead set (vectorConfig/fileAttachmentConfig/dependencies, then referenceFilters/columnName/index) REMOVED (#2377); columnName also dropped the ADR-0062 D7 lint + StorageNameMapping column helpers | | flow | 27 | – | 4 | – | dead = description/template/nodes.outputSchema/errorHandling.fallbackNodeId (engine uses fault edges) | | action | 35 | 1 | 0 | – | `disabled` went LIVE via metadata-admin authoring UI (2026-06 audit missed objectui); `type:'form'` CORRECTED to live (objectui ActionRunner.executeForm, #2377); dead `timeout` REMOVED (#2377) | diff --git a/packages/spec/liveness/object.json b/packages/spec/liveness/object.json index a5a0258f52..3fe240f063 100644 --- a/packages/spec/liveness/object.json +++ b/packages/spec/liveness/object.json @@ -177,16 +177,6 @@ "status": "live", "evidence": "packages/plugins/plugin-audit/src/audit-writers.ts (writeAudit: explicit activities:false skips the sys_activity timeline mirror; audit row unaffected); objectui RecordDetailView skips the timeline merge on explicit false. Opt-out — spec default true; the per-object lever for activity-row growth, ADR-0057 (#2707)." }, - "trash": { - "status": "dead", - "evidence": "no behavior-changing reader", - "note": "Kept `dead` (not experimental): enable.trash defaults to true, and the author-side liveness lint auto-warns on experimental props — for a default-true boolean it cannot distinguish an authored value from the schema default, so tagging it experimental would warn on every object (noise). Disposition for #1893 is prune-or-build, tracked in the sub-issue; no authorWarn so it stays silent." - }, - "mru": { - "status": "dead", - "evidence": "no behavior-changing reader", - "note": "Kept `dead` (not experimental) for the same default-true-boolean reason as enable.trash; disposition tracked in #1893. No authorWarn so it stays silent." - }, "clone": { "status": "live", "evidence": "packages/objectql/src/protocol.ts:2259", diff --git a/packages/spec/src/data/object.form.ts b/packages/spec/src/data/object.form.ts index 28671612de..84d147cca3 100644 --- a/packages/spec/src/data/object.form.ts +++ b/packages/spec/src/data/object.form.ts @@ -190,8 +190,6 @@ export const objectForm = defineForm({ { field: 'files', type: 'boolean' }, { field: 'feeds', type: 'boolean' }, { field: 'activities', type: 'boolean' }, - { field: 'trash', type: 'boolean' }, - { field: 'mru', type: 'boolean' }, { field: 'clone', type: 'boolean' }, ], }, diff --git a/packages/spec/src/data/object.test.ts b/packages/spec/src/data/object.test.ts index e2f168f91a..3779b39b15 100644 --- a/packages/spec/src/data/object.test.ts +++ b/packages/spec/src/data/object.test.ts @@ -10,11 +10,9 @@ describe('ObjectCapabilities', () => { expect(result.apiEnabled).toBe(true); expect(result.files).toBe(false); // feeds/activities are opt-OUT capabilities (#2707): default on, consumers - // gate on explicit `false` only — same posture as trash/mru/clone. + // gate on explicit `false` only — same posture as clone. expect(result.feeds).toBe(true); expect(result.activities).toBe(true); - expect(result.trash).toBe(true); - expect(result.mru).toBe(true); expect(result.clone).toBe(true); }); @@ -26,14 +24,30 @@ describe('ObjectCapabilities', () => { files: true, feeds: true, activities: false, - trash: false, - mru: true, clone: true, }; const result = ObjectCapabilities.parse(capabilities); expect(result).toEqual(capabilities); }); + + // #2377 (ADR-0049): `trash`/`mru` parsed-but-did-nothing for years — the + // retired keys must fail loudly with the upgrade prescription, not strip + // silently (#1535; pattern of the tenancy tombstones, #2763). + it('rejects the retired trash/mru flags with upgrade guidance', () => { + for (const key of ['trash', 'mru'] as const) { + const result = ObjectCapabilities.safeParse({ [key]: true }); + expect(result.success).toBe(false); + const message = result.success ? '' : result.error.issues.map((i) => i.message).join('\n'); + expect(message).toContain(`\`${key}\``); + expect(message).toContain('#2377'); + } + }); + + it('rejects unknown capability keys instead of stripping them', () => { + const result = ObjectCapabilities.safeParse({ feedEnabled: true }); + expect(result.success).toBe(false); + }); }); describe('LifecycleSchema (ADR-0057)', () => { @@ -459,8 +473,7 @@ describe('ObjectSchema', () => { searchable: true, apiEnabled: true, files: true, - feedEnabled: true, - trash: true, + feeds: true, }, }; @@ -544,8 +557,7 @@ describe('ObjectSchema', () => { searchable: true, apiEnabled: true, files: true, - feedEnabled: true, - trash: true, + feeds: true, }, }; @@ -608,8 +620,7 @@ describe('ObjectSchema', () => { searchable: true, apiEnabled: true, files: false, - feedEnabled: false, - trash: true, + feeds: false, }, }; diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index b2838ec948..74836db358 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -26,6 +26,49 @@ export const ApiMethod = z.enum([ ]); export type ApiMethod = z.infer; +/** + * Tombstones for RETIRED capability flags — same doctrine as the tenancy + * block and the top-level `UNKNOWN_KEY_GUIDANCE` map below: a retired + * key's rejection must carry the upgrade prescription, because the parse + * error is the one channel every consumer bumping `@objectstack/spec` is + * guaranteed to hit. Removed in the 16.x line (#2377, ADR-0049 + * enforce-or-remove). + */ +const CAPABILITIES_RETIRED_KEY_GUIDANCE: Record = { + trash: + '`enable.trash` was removed from @objectstack/spec in the 16.x line (#2377, ' + + 'ADR-0049) — it never had a runtime consumer: every delete has always been a ' + + 'hard delete, and a default-true flag promising a recycle bin was a false ' + + 'affordance (authors wrote `trash: false` believing they were opting out of a ' + + 'soft-delete that never ran). Delete the key. For recoverability use per-field ' + + '`trackHistory` (audit trail) or a `lifecycle` policy; a real recycle bin, if ' + + 'built, returns as a live enforced flag (#1893 prune-or-build).', + mru: + '`enable.mru` was removed from @objectstack/spec in the 16.x line (#2377, ' + + 'ADR-0049) — Most-Recently-Used tracking was never implemented; no reader ' + + 'existed, so the flag changed nothing. Delete the key. If MRU tracking is ' + + 'built it returns as a live enforced flag (#1893 prune-or-build).', +}; + +/** + * Custom zod `error` for the `.strict()` capabilities block (pattern of + * `strictTenancyError` below): an unknown key — a retired `trash`/`mru` or a + * typo like `feedEnabled` — is a loud, *fixable* parse error instead of a + * silent strip (#1535), and a retired key's error carries its upgrade + * prescription. Every other issue code defers to zod's default. + */ +const strictCapabilitiesError: z.core.$ZodErrorMap = (issue) => { + if (issue.code !== 'unrecognized_keys') return undefined; + const keys = (issue as { keys?: readonly string[] }).keys ?? []; + const lines = keys.map((key) => + CAPABILITIES_RETIRED_KEY_GUIDANCE[key] ?? `\`${key}\` is not an \`enable\` capability flag.`, + ); + return ( + `Unrecognized key(s) on \`enable\`: ${keys.map((k) => `\`${k}\``).join(', ')}.\n` + + lines.map((l) => ` • ${l}`).join('\n') + ); +}; + /** * Capability Flags * Defines what system features are enabled for this object. @@ -35,10 +78,13 @@ export type ApiMethod = z.infer; * defined enforcement contract (#2707); a flag with no runtime consumer is a * bug, not a reservation — see `@objectstack/spec/liveness/object.json`. * - * Opt-out flags (`feeds`, `activities`, `trash`, `mru`, `clone`, `searchable`, - * `apiEnabled`) default to `true`: absent block/flag = enabled, and consumers - * gate on explicit `false` only. Opt-in flags (`trackHistory`, `files`) - * default to `false`. + * Opt-out flags (`feeds`, `activities`, `clone`, `searchable`, `apiEnabled`) + * default to `true`: absent block/flag = enabled, and consumers gate on + * explicit `false` only. Opt-in flags (`trackHistory`, `files`) default to + * `false`. + * + * `.strict()`: unknown keys (incl. the retired `trash` / `mru`, #2377) are + * rejected with guidance, not stripped (#1535). * * @example * { @@ -106,15 +152,9 @@ export const ObjectCapabilities = z.object({ */ activities: z.boolean().default(true).describe('Record activity timeline (sys_activity mirror of CRUD). Default on; explicit false stops mirroring and hides the timeline'), - /** Enable Recycle Bin / Soft Delete */ - trash: z.boolean().default(true).describe('Enable soft-delete with restore capability'), - - /** Enable "Recently Viewed" tracking */ - mru: z.boolean().default(true).describe('Track Most Recently Used (MRU) list for users'), - /** Allow cloning records */ clone: z.boolean().default(true).describe('Allow record deep cloning'), -}); +}, { error: strictCapabilitiesError }).strict(); /** * Schema for database indexes. diff --git a/skills/objectstack-api/SKILL.md b/skills/objectstack-api/SKILL.md index 3f919c69ea..76f7122e87 100644 --- a/skills/objectstack-api/SKILL.md +++ b/skills/objectstack-api/SKILL.md @@ -51,7 +51,7 @@ GET /api/v1/data/{object} # List records (with filter, sort, paginat GET /api/v1/data/{object}/:id # Get single record POST /api/v1/data/{object} # Create record PATCH /api/v1/data/{object}/:id # Update record -DELETE /api/v1/data/{object}/:id # Delete record (soft-delete if trash enabled) +DELETE /api/v1/data/{object}/:id # Delete record (hard delete) POST /api/v1/data/{object}/query # Complex queries + aggregation (QueryAST in body) POST /api/v1/data/{object}/batch # Per-object batch operations POST /api/v1/batch # Cross-object atomic batch @@ -183,7 +183,7 @@ access: | `aggregate` | No dedicated route — use `POST /data/{object}/query` with `groupBy`/`aggregations` | Count, sum, avg, min, max | | `history` | Enum value gating access — no dedicated generated route today | Audit trail access | | `search` | Global `GET /api/v1/search` (cross-object), not per-object | Full-text search | -| `restore` | Enum value gating access — no dedicated generated route today | Restore from trash | +| `restore` | Enum value gating access — no dedicated generated route today | Restore a soft-deleted record (reserved — platform deletes are hard today) | | `purge` | Enum value gating access — no dedicated generated route today | Permanent deletion | | `import` | `POST /data/{object}/import` | Bulk data import | | `export` | `GET /data/{object}/export` | Data export | @@ -451,9 +451,10 @@ async function firstTenAccounts() { locking (version field) and return `409` on conflict. 4. **Ignoring rate limiting.** Always configure rate limits for public and external-facing APIs. -5. **Using `DELETE` for soft-delete.** ObjectStack `DELETE` performs soft-delete - when `trash: true` is enabled on the object. Do not implement soft-delete - logic in custom endpoints — use the built-in mechanism. +5. **Assuming `DELETE` is recoverable.** ObjectStack `DELETE` is a hard + delete — there is no recycle bin (the dead `enable.trash` flag was removed + in 16.x, #2377). For recoverability, use per-field `trackHistory` (audit + trail) or a `lifecycle` archive policy instead of custom soft-delete logic. --- diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index 39d86a790c..75cf2a7f5c 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -96,8 +96,6 @@ Toggle system behaviours per object: | `files` | `false` | Attachments & document management | | `feeds` | `true` | Social feed, comments, mentions — **opt-out**: explicit `false` hides the feed UI and rejects new comments | | `activities` | `true` | Activity timeline (`sys_activity` mirror of CRUD) — **opt-out**: explicit `false` stops mirroring and hides the timeline | -| `trash` | `true` | Soft-delete with restore | -| `mru` | `true` | Most Recently Used tracking | | `clone` | `true` | Record deep cloning | --- @@ -227,7 +225,6 @@ export default ObjectSchema.create({ trackHistory: true, feeds: true, activities: true, - trash: true, }, fields: { subject: { type: 'text', required: true, maxLength: 255 }, @@ -802,7 +799,6 @@ export const SetupApp = defineApp({ |:--------|:-----------------| | `tenancy` | Multi-tenant SaaS — `{ enabled: true, tenantField: 'tenant_id' }` row-level isolation (DB-per-tenant is an environment/deployment choice, not object metadata) | | `lifecycle` | Append-only / high-write-rate objects — retention / rotation / archival contract (ADR-0057); see [rules/lifecycle.md](./rules/lifecycle.md) | -| `enable.trash` | Soft-delete with restore — defaults on; explicit `false` disables the recycle bin | | per-field `trackHistory` | Render a field's value changes as human-readable activity-timeline entries (pair with `enable.trackHistory`, ADR-0052 §5b) | > The former `softDelete` / `versioning` object keys were **removed** from the