Skip to content

Commit c5f0877

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-vrj8cl
# Conflicts: # packages/spec/src/api/package-lifecycle.zod.ts
2 parents 18ad0f5 + b3d1415 commit c5f0877

343 files changed

Lines changed: 15763 additions & 3258 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@object-ui/collaboration': patch
3+
'@object-ui/types': patch
4+
---
5+
6+
refactor(collaboration, types)!: retire the zero-consumer `useMentionNotifications` hook and the `MentionNotification` interface
7+
8+
Clause-②: no (narrowing)
9+
Metadata-protocol: none
10+
11+
<!-- adr-0087: not-required (runtime-export-retired @object-ui/collaboration#useMentionNotifications, @object-ui/collaboration#MentionNotificationsConfig, @object-ui/collaboration#MentionNotificationsResult, @object-ui/types#MentionNotification) the retired operands are a React hook that kept an in-memory list of mention notifications, its two TypeScript config and result interfaces, and the `MentionNotification` interface of `@object-ui/types` that only the hook read; nothing in the workspace or in hotcrm imports any of them (the console bell reads the server inbox through `useInboxBell`), and no packages/spec schema, registered node type, conversion surface or stored shape names them, so there is nothing for `objectstack migrate meta` to rewrite -->
12+
13+
**BREAKING** for a host that imports `useMentionNotifications`, `MentionNotificationsConfig` or `MentionNotificationsResult` from `@object-ui/collaboration`, or the type `MentionNotification` from `@object-ui/types`. The hook held a client-side list of mention notifications with read flags; no console surface used it, because mention notifications are server records (`sys_notification`, topic `collab.mention`) that the console bell reads from the inbox feed. `CommentEntry` and `ActivityEntry` stay exported unchanged. The `CommentSearchResult` entry of this release said `MentionNotification` stays exported; it leaves here.
14+
15+
- FROM `useMentionNotifications` TO a local hook in your own code, or read the server inbox (`GET /api/v1/notifications`) as the console bell does.
16+
- FROM `MentionNotificationsConfig` TO a local interface in your own code: `{ currentUserId: string; initialNotifications?: MentionNotification[]; onDeliver?: (notification: MentionNotification) => void | Promise<void> }`.
17+
- FROM `MentionNotificationsResult` TO a local interface in your own code, typed by what your own hook returns.
18+
- FROM `MentionNotification` TO a local interface in your own code: `{ id: string; type: 'mention'; recipientId: string; commentId: string | number; mentionedBy: string; commentText: string; objectName?: string; recordId?: string; createdAt: string; read?: boolean; channels?: Array<'in_app' | 'email' | 'push'> }`.

‎.changeset/28001-cascade-delete-objects-beside-item.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ Clause-②: yes (widening)
1111
Metadata-protocol: added
1212
Skills-docs: none — `cascadeDeleteObjects` is a served-only key beside the object read with no authorable spelling; no file under `skills/**` teaches the object read's envelope keys (a `git grep` of `skills/` for `cascadeDeleteObjects` and `disabledActions` names nothing), and every taught `deleteBehavior` use stays true.
1313

14-
- **Served beside the object** (`@objectstack/metadata-protocol`, `@objectstack/rest`). `GET /api/v1/meta/object/:name` serves `cascadeDeleteObjects` beside `item`: the names of the objects that deleting a record of this object can also delete. That is every object whose relation into it cascades (a `master_detail` by default, a `lookup` or `tree` that declares `deleteBehavior: 'cascade'`), and the objects that cascade into those in turn, each once. A `restrict` or `set_null` relation removes nothing and is not named. The default cached door carries it too, and the list is folded into that door's ETag, so an object registered later that cascades into this one changes the ETag. The key is absent where no engine answered, on a draft read, on the list read and on every type other than `object`; `[]` means nothing cascades. `item` is unchanged. A delete confirmation can now name what else a delete removes without keeping its own copy of the rule.
14+
- **Served beside the object** (`@objectstack/metadata-protocol`, `@objectstack/rest`). `GET /api/v1/meta/object/:name` serves `cascadeDeleteObjects` beside `item`: the names of the objects that deleting a record of this object can also delete. That is every object whose relation into it cascades (a `master_detail` by default, a `lookup`, `tree` or `user` field that declares `deleteBehavior: 'cascade'`), and the objects that cascade into those in turn, each once. A `restrict` or `set_null` relation removes nothing and is not named. The default cached door carries it too, and the list is folded into that door's ETag, so an object registered later that cascades into this one changes the ETag. The key is absent where no engine answered, on a draft read, on the list read and on every type other than `object`; `[]` means nothing cascades. `item` is unchanged. A delete confirmation can now name what else a delete removes without keeping its own copy of the rule.
1515
- **The fact it serves** (`@objectstack/objectql`). `ObjectQL.getCascadeDeleteObjects(object)` answers that list from the delete walk's own relation scan, which the delete and this read now share. It reads the registry and no row, so it never says which records or how many. It returns `undefined` for an object the registry does not hold.
1616
- **The declaration** (`@objectstack/spec`). `GetMetaItemResponseSchema` and `GetMetaItemCachedResponseSchema` gain the optional `cascadeDeleteObjects` (an array of object names), and `IObjectQLEngine` gains the optional member `getCascadeDeleteObjects(objectName)`, which the metadata read calls through the kernel's service table. Nothing about it is authorable.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/runtime': patch
3+
'@objectstack/spec': patch
4+
---
5+
6+
feat(runtime): the customizations read serves the `nav` contributions: the navigation items other packages add to a base's apps
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
`GET /packages/:id/customizations` now lists, in `contributions`, the navigation items other installed packages add to the base's apps, each naming the package that adds them. Studio can draw those menu entries greyed with their source, as it already can for contributed fields and flows (ADR-0029 D7 as amended by ADR-0146 D3).
12+
13+
- **One entry per item.** Every item of a package's `navigationContributions` entry that declares an `id`, contributed to an app the base ships, is a `nav` entry: `{ kind: 'nav', app, group?, name, packageId }`. `group` is the group the contribution targets, when it targets one. A separator without an `id` is not an entry, and a contributed group's children ride inside it.
14+
- **The same answer the menu shows.** The entries come from the registry the served app's navigation fold reads, never a join on names. A disabled package's items are not listed, as the fold drops them, and they come back when it is enabled again. The base's own items and its `<base>.custom` pair's are not contributions, and no nav item gains a key.
15+
- **Order and counts.** The `field` and `flow` entries come first as before, then the `nav` entries by app, group (no group first), name and package. `counts.contributions` counts them all.
16+
17+
The spec's `nav` arm drops its `[PLANNED — not yet served]` marker, and the member's descriptions no longer say the kind is unserved. The schema accepts exactly what it accepted before.
18+
19+
Nothing to do for existing metadata. A client that read `contributions` and assumed only `field` and `flow` entries now also receives `nav` entries, which the declared schema already allowed.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
feat(console): the installer OAuth client form shows the deployment's redirect URI, and a reconnect keeps the stored client unless it is replaced
7+
8+
Clause-②: yes
9+
Metadata-protocol: none
10+
11+
On Setup → Integrations → Connector Connections, the client form of an instance declaring `clientSource: 'installer'` now shows the redirect URI to register with the vendor's OAuth client, exactly as the connection read answers it (`redirectUri`), with a Copy button. When the read answers none (the deployment has no `OS_BASE_URL` or `OS_AUTH_URL` to derive it from), the form says so and shows no URI; nothing is guessed from the browser's address. A reconnect of a connection that keeps an installer client now keeps it by default: the connect is begun with no body and the server reuses the stored client and secret. "Replace the stored client" shows the client ID (prefilled) and an empty secret field and sends the new client. After a disconnect, the page re-reads the connection, so the next Connect still shows the redirect URI.
12+
13+
`@object-ui/i18n`, under `connectorConnections` in all ten locales: eight new keys (`redirectUriLabel`, `redirectUriCopy`, `redirectUriCopied`, `redirectUriHint`, `redirectUriMissing`, `clientKeepHint`, `clientKeep`, `clientReplace`), and reworded values of `clientIntro` (now points at the redirect URI below instead of the documentation) and `clientResend` (now the replace hint). No key is removed.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/runtime': patch
3+
'@objectstack/client': patch
4+
'@objectstack/cli': patch
5+
'@objectstack/spec': patch
6+
---
7+
8+
feat(runtime,client,cli): export a deployment's whole ontology as installable packages in one read (`GET /packages/export`, `os package export`)
9+
10+
Clause-②: yes (widening)
11+
Metadata-protocol: none
12+
13+
A tenant could take its ontology out of a deployment only one package at a time (`GET /packages/:id/export`). The whole-deployment export declared in `@objectstack/spec` (`PackageExportBundleSchema`) is now served.
14+
15+
- **`@objectstack/runtime`**: `GET /api/v1/packages/export` answers `{ packages }`: every installed package whose registry row's `source` is not `code` (the marketplace and authored packages, each base's `.custom` pair among them), disabled ones included, each exactly as `GET /packages/:id/export` answers it, dependencies first, so a pair follows its base. It reads with the per-package export's gate (`studio.access` or `setup.access`), refuses any query parameter `400 VALIDATION_ERROR` naming it, and is whole or absent: a package whose export cannot be assembled fails the read, naming that package. No credential value is carried: credential positions are withheld as on every served metadata read, and a connector's `credentialRef` stays a reference.
16+
- **`@objectstack/client`**: `client.packages.exportAll()` calls it.
17+
- **`@objectstack/cli`**: `os package export [--out DIR]` writes one directory per package, numbered in install order (`DIR/01-ID/objectstack.json`), each installable with `os package install`. A refusal is printed with its status and code and nothing is written; an `--out` directory that already holds files is refused.
18+
- **`@objectstack/spec`**: the `[PLANNED — not yet served]` marker comes off `PackageExportBundleSchema`'s description, which now names the route and the command.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/service-messaging': patch
3+
'@objectstack/runtime': patch
4+
'@objectstack/client': patch
5+
'@objectstack/spec': patch
6+
---
7+
8+
feat(notifications): the push device registry is served: `sys_user_device`, `POST /api/v1/notifications/devices` and `DELETE /api/v1/notifications/devices/:deviceId`, and the client's `notifications.registerDevice` / `unregisterDevice` (ADR-0012 §6)
9+
10+
Clause-②: yes (widening)
11+
Metadata-protocol: none
12+
13+
Nothing a client or an operator sends today is refused, and no metadata an author writes changes: the routes and the client methods are new, and the spec change is two catalog rows and description text.
14+
15+
- **`@objectstack/service-messaging`** registers `sys_user_device` (one row per user and app install: `device_id`, `user_id`, `vendor`, `token`, `platform`, `app_id`, `locale`, `name`, `enabled`, `invalidated_at`, `last_seen_at`) and implements `INotificationService`'s four device members on `MessagingService`. The table is system-owned: the generic data API neither reads nor writes it (`apiEnabled: false`), and it has no organization column, because a device belongs to its user. `registerDevice` refreshes a device id the user already holds, mints one when the request carries none, and moves a token another device holds to the registering device (the previous holder's row is removed, so a push meant for a phone's previous user never reaches its new one). A token has one holder: a unique index on `token` refuses a second one, and a registration that loses a race re-reads and moves the token. `invalidateDeviceToken` keeps the row with `enabled: false` and `invalidated_at`, and the device's next registration re-enables it. A `web` registration without `vendor` is refused `400 VALIDATION_FAILED`.
16+
- **`@objectstack/runtime`** serves `POST /api/v1/notifications/devices` (body: `RegisterDeviceRequestSchema`; answers `{ deviceId, success }`) and `DELETE /api/v1/notifications/devices/:deviceId` (the caller's own device; another user's or an unknown id answers `404 NOT_FOUND` and nothing is removed), behind the domain's anonymous floor (`401 UNAUTHENTICATED`). A notification service without the device members answers `501`. The standalone server mounts both, with the other verbs on the two paths mounted refusal-only like the inbox faces.
17+
- **`@objectstack/client`**: `client.notifications.registerDevice(request)` and `client.notifications.unregisterDevice(deviceId)`, removed earlier because no server served them, are back.
18+
- **`@objectstack/spec`**: `DEFAULT_NOTIFICATION_ROUTES` lists the two device routes again, and the `[PLANNED — not yet served]` markers come off the four device registration schemas and `UserDeviceSchema`.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): objectstack-formula names action `visible` / `disabled` as the sparse, client-side exception to the total-record rule, and teaches the `has()` guard that surface needs
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** The "`has()` is NOT a null check" section said every predicate reads a `record` that is total over the object's declared fields, so `has(record.<declared_field>)` is always `true` and tells you nothing. An agent following it dropped the `has()` guard from an action's `visible` predicate on a `list_item` location, where the row can lack the key: the predicate faults `No such key` and the button silently vanishes. The Surfaces table listed only `Action.disabled`.
11+
- **Now.** The total-record statement is scoped to the server-evaluated surfaces (validation rules, `requiredWhen`, option `visibleWhen`, `readonlyWhen`, hook `condition`, the flow trigger record), where `!= null` is the whole guard. A new paragraph says action `visible` / `disabled` bind the row the client already holds, which lacks a key when the column is outside the view's projection, refused by field-level security, or never written (the mongo and memory drivers), and teaches the guard: `has(record.x) && record.x != null`; `has()` alone beside a literal `==` / `!=`; one `has()` per segment of a nested path. The Surfaces table lists `visible` beside `disabled`, marked client-side and sparse.
12+
- `SKILL.md` stays under its token ceiling (no ceiling moves). No schema, key, command or behaviour changes.

‎.changeset/29253-user-field-delete-behavior.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ fix(objectql)!: a `user` field's declared `deleteBehavior` takes effect when the
77
Clause-②: no (narrowing) — deleting a user that a `user` field declaring `restrict` (or `set_null` with `required: true`) holds is refused with `409 DELETE_RESTRICTED`, where it succeeded before; a declared `cascade` now deletes the rows holding the user, and a declared `set_null` now clears them. Nothing is widened.
88
Metadata-protocol: semantic
99

10-
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export or type changes: a `deleteBehavior` the spec already accepts on a `user` field is now enforced by the delete walk the way it is on a `lookup`. The refusal is the walk's own DELETE_RESTRICTED, so there is no stored value for a conversion to rewrite; an author who did not mean the declaration removes it or changes it. -->
10+
<!-- adr-0087: registered user-field-delete-behavior-enforced -->
1111

1212
**BREAKING** for an app that declares `deleteBehavior` on a `user` field: the declared value now takes effect when the user is deleted.
1313

@@ -17,6 +17,6 @@ A `user` field accepts `deleteBehavior`, but the referential delete walk only re
1717
- **Unchanged.** A `user` field that declares no `deleteBehavior` (the spec's parse adds none) stays out of the walk: deleting the user leaves the field as it was. `lookup`, `master_detail` and `tree` behave as before.
1818
- **Which deletes.** Every user delete: better-auth deletes the user (admin remove-user, self delete-user) through the engine by id, and the generic data routes refuse a caller's own `sys_user` delete. A `restrict` refusal reaches the auth caller as `409 DELETE_RESTRICTED`, and the credential rows deleted before it are rolled back.
1919

20-
- **Rows stored before #9784.** Until then the spec's parse wrote `deleteBehavior: 'set_null'` on every field type, `user` included, so an object stored in that era can carry a `set_null` on a `user` field that no author wrote. That value now takes effect: the field is cleared when the user is deleted, and on a `required` field the delete is refused.
20+
- **Rows stored before #9784.** Until then the spec's parse wrote `deleteBehavior: 'set_null'` on every field type, `user` included, so an object stored in that era can carry a `set_null` on a `user` field that no author wrote. That value now takes effect: the field is cleared when the user is deleted, and on a `required` field the delete is refused. No conversion rewrites it, because whether the value was meant is the author's to say: the semantic migration entry `user-field-delete-behavior-enforced` lists it for `objectstack migrate meta` and the upgrade guide. Save the object again without the key, or with the value you want.
2121

2222
**The fix.** If you declared `deleteBehavior` on a `user` field only as documentation, remove it or change it to what you want enforced. Nothing to do for a `user` field that declares no `deleteBehavior`.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): register the `user`-field `deleteBehavior` enforcement as a semantic migration entry, and stop the spec saying a `user` field never joins the delete walk (#29415)
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The enforcement itself shipped in `@objectstack/objectql` (#29253): a `deleteBehavior` declared on a `user` field now takes effect when the user is deleted. This change records it and corrects the spec text around it. Nothing an author can write, and nothing the runtime does, moves.
11+
12+
- **Migration entry.** A new semantic entry, `user-field-delete-behavior-enforced`, so `objectstack migrate meta` and the protocol upgrade guide reach the change. It names the one stored population that needs a look: objects saved while the parse still wrote `deleteBehavior: 'set_null'` on every field type can carry a `set_null` on a `user` field nobody wrote. On a field that is not `required` it clears the field when the user is deleted and is fine as it is; on a `required` field it now refuses the user delete with `409 DELETE_RESTRICTED`, so remove it or declare the outcome you want.
13+
- **Spec text.** The `deleteBehavior` docblocks in `FieldSchema`, the `user`-field `reference` refusal message, the `user-field-foreign-reference-refused` entry, the `referentialFieldClear` and `referentialCascadeDelete` hook-context descriptions, the `getCascadeDeleteObjects` contract docblock and the `cascadeDeleteObjects` description on the metadata `object` read now say that a `user` field joins the delete walk when it declares a `deleteBehavior`, and stays out when it declares none.
14+
- **Studio forms.** The field designer and the object form's field rows now show the `deleteBehavior` control on a `user` field, with all three outcomes, as they do on a `lookup`. A `user` field has no default, so the control writes a value only when you pick one.

0 commit comments

Comments
 (0)