You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 0b45222
Browse filesBrowse the repository at this point in the historyBrowse files
perf(formula): a CEL evaluation reuses one environment instead of building the stdlib on every call, and compiles reuse the canonical parse environment
6
+
7
+
Clause-②: no (an internal cache: no accept set moves, no evaluation result moves, no exported signature changes; owes the seat's review of record before the queue)
8
+
9
+
-**`celEngine.evaluate` reuses one environment.** It used to build a new cel-js `Environment` and register the whole stdlib into it on every call, which objectql's formula fields reach once per row per field. The stdlib now reads `now()`, the timezone and the permission binding of `current_user.can` / `current_user.hasCapability` from a per-evaluation slot. `evaluate` saves the slot, sets it and restores it in a `finally`, so a nested or a thrown evaluation leaves the outer binding as it was. Evaluations also run a bounded per-source cache of parsed programs, keyed on the source after its row-dependent rewrites.
10
+
-**`celEngine.compile` reuses the canonical parse environment** that `parseCelToAst` already shares.
11
+
- Results, error kinds and error messages are unchanged. `registerStdLib`, `registerPermissionPredicate` and `registerCapabilityPredicate` keep their signatures and their behaviour.
12
+
- Measured on one source evaluated 20,000 times: about 142 µs per evaluation before, about 3 µs after. A 25-row read of an object with three formula fields built 78 environments before and builds none after.
The `sys_attachment` parent gate and the `sys_comment` moderation gate give the answer a by-id update of the parent record gets
7
+
8
+
Clause-②: no (narrowing)
9
+
10
+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling or stored shape is removed, renamed or re-shaped, and no stored row is read, rewritten, converted or dropped, so there is nothing for `objectstack migrate meta` to rewrite. What narrows is a runtime accept set: the attachment parent gate and the comment moderation gate admit only where a by-id update of the parent record would be admitted. The remedy for a refused caller is an access decision on the parent record, not a rewrite. The exported port types AttachmentSecurityLike and CommentSecurityLike gain one optional member, which no caller has to supply. The other categories are closed on facts: both bumped packages publish (not unpublished); no ADR-0087 id covers these paths and this diff adds none (not registered, not already-registered); the port types stay concretely typed (not type-surface-only). -->
11
+
12
+
- Every limb that asks whether the caller may edit the parent record (attaching a file, editing or removing another user's attachment or comment, moving an attachment to another record, and `assertMayAttach`) now admits only where a by-id update of that parent admits. After record sharing and the master-detail check, the gate asks the security service's judgement of that update: its record-grained `explain` verdict for `update` on the parent, `allowed` and `record.visible` both.
13
+
- The step only adds refusals, with each limb's existing envelope (`ATTACHMENT_PARENT_ACCESS`, `ATTACHMENT_DELETE_DENIED`, `RECORD_NOT_ACCESSIBLE`, or the not-visible `PERMISSION_DENIED` for a caller who cannot read the parent). A store fault keeps its own status, and an attach naming a parent object the registry does not hold answers what a by-id update of it answers (`404 OBJECT_NOT_FOUND`).
14
+
- Posting a comment is unchanged: it stays a read rule on the parent.
fix(cli): `os migrate apply --allow-destructive`'s organization-ownership refusal says what 18.0 supports for a v17 database, and names no step of the command 18.0 refuses
6
+
7
+
Clause-②: no
8
+
9
+
-**`os migrate apply --allow-destructive` on a v17-shaped database.** When the run would drop `organization_id` from a table the organization-ownership ceremony owns, and the ceremony has not verified on the database, the refusal now says that 18.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17.x release until the 18.x release that carries the upgrade path, whose ceremony drops these columns itself. The v18 boot refusal and `os migrate organization-ownership`'s own refusal say the same. Before, it told the operator to run `os migrate organization-ownership --out PLAN` and then `--apply --plan-file PLAN`, which 18.0 refuses on every database (`NOT_IMPLEMENTED`). The `--json` refusal's `detail` no longer names that command either: it reads "No run of the organization-ownership ceremony is recorded on this database.", or names the last unverified run with its date and blocking count.
10
+
-**`os migrate organization-ownership --help`** shows what the command answers in 18.0: exit `1` with `NOT_IMPLEMENTED`, nothing read or written, and the same refusal as JSON. Before, it showed six plan and apply examples that 18.0 refuses.
11
+
-**`os dev` on a v17-shaped development database.** When it does not upgrade the database in place (the ceremony's plan is refused, or the database is not a local SQLite file), it now ends with the same position: keep a v17 development database on a 17.x release until the 18.x release that carries the upgrade path, and upgrade it there. Before, it told the developer to run `os migrate organization-ownership --dev` and then the same command with `--apply`.
12
+
13
+
Nothing else changes. The refusals still hold back the whole run, and their message code (`refused_organization_ownership_unmigrated`), `tables`, `marker`, exit code `1` and every flag are as before.
fix(objectql): a `date` dimension buckets on its own calendar day under every timezone on the in-memory path, as the SQL pushdown path already did
6
+
7
+
Clause-②: no (no authorable key, export or signature moves; the in-memory answer for a `date` dimension under a non-UTC zone now equals the stored day, which is what the pushdown path answers)
8
+
9
+
-**A `Field.date` is a calendar day, not an instant (ADR-0053).** When `engine.aggregate` buckets in memory (any non-UTC `timezone` forces that path), a `groupBy` item with a `dateGranularity` on a field the object declares a `date` is now labelled by that field's own `YYYY-MM-DD`, with no zone. Before, the value was read as that day's UTC midnight and placed on the requested zone's calendar, so in every zone west of UTC each row landed one day early: `2026-10-01` bucketed as `2026-09-30`, and month, quarter and year totals moved across their edges (`POST /api/v1/analytics/query` with `timezone: "America/New_York"` answered `{2026-09: 1, 2026-10: 5, 2026-11: 4}` for six October and four November deliveries).
10
+
-**A `datetime` keeps the zone.** An instant still lands on the requested zone's calendar day, unchanged. So does a field the declaration cannot type: `applyInMemoryAggregation` called without its `fields` argument, an undeclared field, or a `formula`.
11
+
-**The value is read by the `date` storage rule first** (`temporalStorageForm(value, 'date')` in `@objectstack/core`): a bare `YYYY-MM-DD` is itself, and a `Date` handed back by a driver is its UTC calendar day.
3. the security service's judgement of that update — the record-grained verdict
53
+
`security.explain` reports for `update` on the parent record (`allowed` and
54
+
`record.visible` both).
55
+
56
+
The same rule governs every check that asks about the parent: attaching,
57
+
detaching or editing another user's attachment, re-pointing an attachment to
58
+
another record (judged on the new parent), and the file-attachments service's
59
+
caller-scoped `assertMayAttach`. It never admits more than the parent's update
60
+
does, and it can refuse more: record sharing is asked at the narrowest edit
61
+
depth for these gates (the ruling recorded on `ISharingService`, "Write DEPTH
62
+
is an input the CALLER supplies").
63
+
64
+
When no sharing service is wired, a caller-scoped read of the parent (a
65
+
`findOne`) stands in for step 1. A kernel with no security service asks no
66
+
update judgement, as the parent's own update meets none there. A store fault in
67
+
step 2 or 3 keeps its own status (a `503` stays a `503`); it is never reported
68
+
as a refusal. `uploaded_by` is **server-stamped** from the session; a
69
+
client-supplied value is ignored.
49
70
50
71
| Code | Status | When |
51
72
| --- | --- | --- |
52
-
|`ATTACHMENT_PARENT_ACCESS`| 403 | The caller cannot access the parent record they are attaching to |
73
+
|`ATTACHMENT_PARENT_ACCESS`| 403 | The parent record does not exist, or a by-id update of it by the caller would not be admitted (on a re-point, the new parent) |
74
+
|`OBJECT_NOT_FOUND`| 404 | The `parent_object` names an object the registry does not hold — the answer a by-id update of it gets |
53
75
54
76
## Read / list — inherited parent visibility
55
77
@@ -65,8 +87,9 @@ resolve the filter fails closed (deny-all).
65
87
## Delete — uploader or parent editor
66
88
67
89
Deleting a `sys_attachment` is allowed when the caller is **the uploader** OR
68
-
**can edit the parent record** (`sharing.canEdit` — public-model parents are
69
-
editable by design). A multi-delete (`multi: true`) matches only attachments
90
+
**may edit the parent record** — the parent-edit rule above: a by-id update of
91
+
the parent by that caller would be admitted. The uploader keeps their own rows
92
+
whatever the parent answers. A multi-delete (`multi: true`) matches only attachments
70
93
the caller can read: an attachment whose parent is out of the caller's sight is
71
94
not matched, not removed and not counted, so a predicate that reaches only such
72
95
attachments answers exactly what a predicate that matches nothing answers. Every
@@ -81,7 +104,15 @@ matched row must then pass.
81
104
An update of another user's attachment follows the same rule — the uploader or
82
105
a parent editor — and a by-id update of an attachment the caller cannot read
83
106
gets the same not-found answer. A multi-update matches only attachments the
84
-
caller can read, as a multi-delete does.
107
+
caller can read, as a multi-delete does. Re-pointing an attachment
108
+
(`parent_object` / `parent_id`) to another record is an attach there: the new
109
+
parent must pass the parent-edit rule too (`ATTACHMENT_PARENT_ACCESS`, 403).
110
+
111
+
The comment thread's moderation gate (`sys_comment`, `@objectstack/plugin-audit`)
112
+
asks the same parent-edit rule: editing or removing another person's comment
113
+
requires that a by-id update of the thread's parent record would be admitted,
114
+
and the author keeps their own comments. Posting a comment stays a read rule on
115
+
the parent.
85
116
86
117
The platform baseline also ships a parent-blind row-level delete floor
87
118
(`owner_only_deletes`: you may delete only the rows you created, for members
@@ -181,7 +212,7 @@ can be shared across records). Reclamation is handled by the platform LifecycleS
| Attach (create)| can edit the parent record |`ATTACHMENT_PARENT_ACCESS` (403) |
215
+
| Attach (create), re-point | a by-id update of the parent record by the caller would be admitted |`ATTACHMENT_PARENT_ACCESS` (403); `OBJECT_NOT_FOUND` (404) when the parent object is not registered|
| Delete | uploader or parent editor (+ RBAC delete grant) |`ATTACHMENT_DELETE_DENIED` (403); `PERMISSION_DENIED` (403) when the parent is not readable or no delete grant is held |
187
218
| Download | session + owner-or-parent-read (attachments scope); session only for a file with no parent; none for `acl: 'public_read'`|`AUTH_REQUIRED` (401) / `ATTACHMENT_DOWNLOAD_DENIED` (403) |
0 commit comments