- @objectstack/spec@15.1.1
- @objectstack/core@15.1.1
- @objectstack/types@15.1.1
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [3fe9df1]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [4109153]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [627f225]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- @objectstack/spec@15.1.0
- @objectstack/core@15.1.0
- @objectstack/types@15.1.0
- Updated dependencies [28b7c28]
- Updated dependencies [13749ec]
- Updated dependencies [e62c233]
- Updated dependencies [ed61c9b]
- Updated dependencies [31d04d4]
- @objectstack/spec@15.0.0
- @objectstack/core@15.0.0
- @objectstack/types@15.0.0
- 84650c5: Log a concise one-liner instead of the full
ERR_DLOPEN_FAILEDstack trace when nativebetter-sqlite3cannot load (an ABI /NODE_MODULE_VERSIONmismatch after a Node upgrade, or the native addon was never built). The native → wasm SQLite step-down is unchanged — this only stops a handled, non-fatal fallback from reading like a fatal crash in the dev console, and points atpnpm rebuild better-sqlite3for native speed. Any otherPRAGMAfailure keeps its full warning. - Updated dependencies [16b4bf6]
- Updated dependencies [16b4bf6]
- Updated dependencies [10e8983]
- Updated dependencies [607aaf4]
- Updated dependencies [bb71321]
- @objectstack/spec@14.8.0
- @objectstack/core@14.8.0
- @objectstack/types@14.8.0
- Updated dependencies [d6a72eb]
- Updated dependencies [824a395]
- @objectstack/spec@14.7.0
- @objectstack/types@14.7.0
- @objectstack/core@14.7.0
-
ce6d151: fix(driver-sql): fail-loud on unknown filter operators; real IS NULL / IS NOT NULL; $not support (#2704)
The SQL driver used to forward any filter operator it didn't recognise straight to Knex. On a null comparand that silently compiled to a whole-table match, so a permission/assignment-scoped list view could leak every row (e.g. an
is_null/is_emptyoperator from the client). It also had no real null-check:field = nullnever rendersIS NULLin SQL.This change makes the driver:
- Render null predicates as real SQL —
is_null/isnull/is_empty(and the not-null variants) →IS NULL/IS NOT NULL, unified withequals+ null;!= null→IS NOT NULL. - Support the full spec operator set plus client alias spellings across both
filter shapes (array
[field, op, value]and object{field: {$op: value}}):$between,$startsWith,$endsWith,$notContains,$null,$exists, and the logical$not(a negated sub-condition, matching driver-mongodb / driver-memory — CEL!exprpermission scopes compile to it). - LIKE-escape
contains/startsWith/endsWithvalues with an explicitESCAPE '\'so%/_in user input can't widen the match. - Throw on a genuinely unknown operator in both paths instead of silently passing it through — no more silent whole-table results.
@objectstack/specrecognises the client alias operator spellings (isnull/is_empty/ …) inVALID_AST_OPERATORSand maps them to$nullso the array-AST → object-filter conversion is consistent with the driver. - Render null predicates as real SQL —
-
Updated dependencies [609cb13]
-
Updated dependencies [ce6d151]
- @objectstack/spec@14.6.0
- @objectstack/core@14.6.0
- @objectstack/types@14.6.0
- Updated dependencies [526805e]
- Updated dependencies [d79ca07]
- Updated dependencies [33ebd34]
- Updated dependencies [c044f08]
- Updated dependencies [01274eb]
- @objectstack/spec@14.5.0
- @objectstack/core@14.5.0
- @objectstack/types@14.5.0
-
7953832: ADR-0057 data lifecycle P1–P4 (#2786): platform-generated data is now bounded by construction.
- P1 — contract: new
lifecycleobject property (class: record | audit | telemetry | transient | event+retention/ttl/storage(rotation)/archive/reclaim), enforced by the platform-owned LifecycleService registered byObjectQLPlugin(default-on; disable viaOS_LIFECYCLE_DISABLED=1or pluginlifecycle.enabled=false). The Reaper batch-deletes rows pastretention.maxAge/ttlunder a system context and reclaims space (SqlDriver.reclaimSpace()→ SQLitePRAGMA incremental_vacuum). Non-recordclasses must declare a bounding policy (parse-time invariant + spec-liveness gate + dogfood storage-growth gate). - P2 — rotation:
storage: { strategy: 'rotation', shards, unit }physically time-shards the table on SQLite — writes land in the current shard, reads go through a UNION-ALL view under the base name, expiry is an O(1)DROPof shards past the window. A legacy table is adopted as the first shard on upgrade. Other dialects fall back to an equivalent age-based reap. - P3 — separation + Archiver: registering a datasource named
telemetryroutes telemetry/event/audit objects to it (opt-in by existence;transientdeliberately stays on the primary). Audit objects witharchivedeclared get retain → archive → delete once the archive datasource exists; without it rows are retained, never dropped unarchived. - P4 — governance: new
lifecyclesettings namespace — runtime enable switch, per-object retention overrides (tenant-scoped: regulated tenants set years, dev sets days), per-object/per-class row quotas and growth alerts (observe-and-alert only).
Behavior change: 11 platform objects now carry lifecycle declarations and their telemetry is bounded by default —
sys_activity14d (rotated),sys_audit_log90d hot → archive (retained forever until anarchivedatasource is registered),sys_metadata_audit365d → archive,sys_job_run/sys_automation_run/sys_http_delivery30d, notification pipeline (sys_notification, delivery, receipt, inbox) 90d,sys_device_codeexpires_at + 1d. Extend windows per environment/tenant via thelifecycle.retention_overridessetting. - P1 — contract: new
- Updated dependencies [7953832]
- Updated dependencies [82e745e]
- Updated dependencies [f3035bd]
- Updated dependencies [82c0d94]
- Updated dependencies [7449476]
- @objectstack/spec@14.4.0
- @objectstack/core@14.4.0
- @objectstack/types@14.4.0
- Updated dependencies [2a71f48]
- Updated dependencies [02f6af4]
- Updated dependencies [c1064f1]
- @objectstack/spec@14.3.0
- @objectstack/core@14.3.0
- @objectstack/types@14.3.0
- Updated dependencies [ac8f029]
- Updated dependencies [4ab9958]
- @objectstack/spec@14.2.0
- @objectstack/core@14.2.0
- @objectstack/types@14.2.0
- Updated dependencies [5a8465f]
- Updated dependencies [7f8620b]
- Updated dependencies [82ba3a6]
- @objectstack/spec@14.1.0
- @objectstack/core@14.1.0
- @objectstack/types@14.1.0
-
afa8115: Three permission-runtime fixes found dogfooding the ADR-0090 showcase zoo:
#2734 — driver tenant wall hid every global row.
applyTenantScopeused strictorganization_id = :tenantIdequality, so any caller with an active org (every logged-in admin) saw ZERO rows in the org-less platform tables (sys_position,sys_permission_set,sys_business_unit— Setup → Access Control rendered empty on a fresh deployment) and none of the first-boot seeds (stamped before the default org exists). The scope is now(organization_id = :tenantId OR organization_id IS NULL): a NULL tenant column marks a GLOBAL/platform row that belongs to no other tenant; rows stamped with a DIFFERENT org stay invisible exactly as before.#2735 — bulkCreate skipped write-side marshaling. The batch insert path (the common case for seeds/imports since #2678) handed raw object values (
location/json/arrayfields) to the SQLite binder — "Wrong API use: tried to bind a value of an unknown type" — silently failing whole seed batches (showcase accounts/tasks/field-zoo seeded zero rows).bulkCreatenow runs each row through the sameformatInput+applyWriteColumnMap+ timestamp-stamp sequence ascreate(), and decodes the read-back the same way.#2737 — count()/aggregate() ignored injected read filters.
engine.countandengine.aggregatebuilt a LOCAL ast inside the executor, discarding the RLS/OWD filters the security and sharing middlewares inject intoopCtx.ast.where—GET /data/:objectreturned scopedrecordswith an UNSCOPEDtotal(a row-count oracle over invisible records, broken pagination). Both now carry their ast on the opCtx exactly likefind(). -
Updated dependencies [0a8e685]
-
Updated dependencies [afa8115]
-
Updated dependencies [80f12ca]
-
Updated dependencies [e2fa074]
-
Updated dependencies [23c8668]
-
Updated dependencies [29f017d]
-
Updated dependencies [216fa9a]
-
Updated dependencies [6c22b12]
- @objectstack/spec@14.0.0
- @objectstack/core@14.0.0
- @objectstack/types@14.0.0
- Updated dependencies [6d83431]
- Updated dependencies [01917c2]
- Updated dependencies [b271691]
- Updated dependencies [a5a1e41]
- Updated dependencies [466adf6]
- Updated dependencies [57b89b4]
- Updated dependencies [5be00c3]
- Updated dependencies [466adf6]
- Updated dependencies [2bee609]
- Updated dependencies [fc7e7f7]
- @objectstack/spec@13.0.0
- @objectstack/core@13.0.0
- @objectstack/types@13.0.0
-
21420d9: Seed loader and data-import now route bulk writes through the engine's array-form
insert()(one round-trip per batch, with parent-deduplicated summary recompute) instead of oneinsert()/createData()call per record, and both retry transient driver errors instead of silently dropping the row (#2678).A new shared helper,
bulkWrite(@objectstack/core), batches rows through a caller-supplied batch-write function, retries a whole-batch transient failure (network blip / timeout) with exponential backoff, and degrades to per-row writes (each itself retried) when a batch fails for a non-transient reason — so one bad row can't drop the other N-1.withTransientRetrywraps a single write (e.g. an update) with the same retry behavior.SeedLoaderService.loadDataset()(@objectstack/metadata-protocol) buffers insert-mode records and flushes them in batches of 200 via the engine's arrayinsert(). Datasets with a self-referencing field (e.g.employee.manager_id -> employee) keep the historical per-record write path, since a later record may need an earlier one's freshly-assigned id.runImport()(@objectstack/rest) buffers create-resolved rows and flushes them viaprotocol.createManyData()when the protocol supports it, falling back to the original per-rowcreateData()call otherwise.Protocol.createManyData(@objectstack/metadata-protocol) now forwardscontexttoengine.insert()likecreateDataalready did, so tenant-scoped bulk creates work correctly.
Previously, a 1000-row seed or import into an object with a rollup summary issued 1000+ round-trips and up to 1000 summary recomputes; a single transient network error on any one row silently dropped it with no retry (the 2026-07-06 HotCRM first-boot incident). A
bulkCreate-capable driver now sees roughlyceil(N/batch)writes, and a transient error is retried before a row is ever reported as failed.Fix (
@objectstack/driver-sql):SqlDriver.bulkCreate()never generated a client-side id for a row missing one, unlikecreate()— a latent gap that this change is the first to exercise at scale (a bulk-inserted row without a driver-native id default silently landed withid: NULL).bulkCreate()now mirrorscreate()'s id/_idnormalization per row. -
Updated dependencies [6cebf22]
-
Updated dependencies [21420d9]
- @objectstack/spec@12.6.0
- @objectstack/core@12.6.0
- @objectstack/types@12.6.0
- Updated dependencies [8b3d363]
- @objectstack/spec@12.5.0
- @objectstack/core@12.5.0
- @objectstack/types@12.5.0
- Updated dependencies [60dc3ba]
- @objectstack/spec@12.4.0
- @objectstack/core@12.4.0
- @objectstack/types@12.4.0
- Updated dependencies [e7eceec]
- @objectstack/spec@12.3.0
- @objectstack/core@12.3.0
- @objectstack/types@12.3.0
- Updated dependencies [fce8ff4]
- Updated dependencies [3962023]
- Updated dependencies [2bb193d]
- Updated dependencies [0426d27]
- Updated dependencies [da807f7]
- Updated dependencies [4f5b791]
- @objectstack/spec@12.2.0
- @objectstack/core@12.2.0
- @objectstack/types@12.2.0
- Updated dependencies [93e6d02]
- @objectstack/spec@12.1.0
- @objectstack/core@12.1.0
- @objectstack/types@12.1.0
- Updated dependencies [a8df396]
- Updated dependencies [e695fe0]
- Updated dependencies [7c09621]
- Updated dependencies [7709db4]
- Updated dependencies [2082109]
- Updated dependencies [7c09621]
- Updated dependencies [9860de4]
- Updated dependencies [069c205]
- @objectstack/spec@12.0.0
- @objectstack/core@12.0.0
- @objectstack/types@12.0.0
- Updated dependencies [6a9397e]
- Updated dependencies [c0efe5d]
- @objectstack/spec@11.10.0
- @objectstack/core@11.10.0
- @objectstack/types@11.10.0
-
8d87930: Fix a connection-pool deadlock when the first
auto_numberwrite after process start goes through a transaction (e.g.POST /api/v1/batch, which wraps every operation in oneql.transaction(...)).The sequence-counter table (
_objectstack_sequences) was created lazily on the first autonumber INSERT via a barethis.knex.schema.*call that asks the pool for a second connection. On SQLite (better-sqlite3, pool max=1) the open batch transaction already holds the only connection, so the acquire blocked untilKnex: Timeout acquiring a connection. Postgres/MySQL are exposed to the same pool-exhaustion deadlock under concurrent cold first-writes.Fixes:
initObjectsnow pre-creates the counter table up front, outside any data transaction, so the first write never runs DDL (primary fix).- The lazy fallback (
ensureSequencesTable) now runs its DDL on the caller's own transaction on SQLite instead of grabbing a second connection. It deliberately does not route DDL through the caller's transaction on MySQL, where DDL would implicitly commit the caller's in-flight transaction. - Added a dev/test guard (
assertBareKnexSafe): on SQLite, issuing a barethis.knexquery while a transaction holds the single pooled connection now fails fast with an actionable error instead of hanging until the opaqueKnex: Timeout acquiring a connection. No-op in production and on non-SQLite dialects, so it adds no runtime cost on the hot path — it just turns this whole class of "forgot to thread the transaction through" bug into an immediate, self-explaining failure at the call site.
-
Updated dependencies [d3595d9]
- @objectstack/spec@11.9.0
- @objectstack/core@11.9.0
- @objectstack/types@11.9.0
- @objectstack/spec@11.8.0
- @objectstack/core@11.8.0
- @objectstack/types@11.8.0
- Updated dependencies [5178906]
- @objectstack/spec@11.7.0
- @objectstack/core@11.7.0
- @objectstack/types@11.7.0
- @objectstack/spec@11.6.0
- @objectstack/core@11.6.0
- @objectstack/types@11.6.0
- Updated dependencies [6ee4f04]
- Updated dependencies [c1e3a65]
- @objectstack/spec@11.5.0
- @objectstack/core@11.5.0
- @objectstack/types@11.5.0
- Updated dependencies [5821c51]
- Updated dependencies [a0fce3f]
- @objectstack/spec@11.4.0
- @objectstack/core@11.4.0
- @objectstack/types@11.4.0
- Updated dependencies [58e8e31]
- Updated dependencies [b4a5df0]
- @objectstack/spec@11.3.0
- @objectstack/core@11.3.0
- @objectstack/types@11.3.0
- Updated dependencies [d0f4b13]
- Updated dependencies [302bdab]
- @objectstack/spec@11.2.0
- @objectstack/core@11.2.0
- @objectstack/types@11.2.0
- Updated dependencies [ce0b4f6]
- Updated dependencies [9ccfcd6]
- Updated dependencies [ecf193f]
- Updated dependencies [51bec81]
- Updated dependencies [3e593a7]
- Updated dependencies [fdb41c0]
- Updated dependencies [63d5403]
- @objectstack/core@11.1.0
- @objectstack/spec@11.1.0
- @objectstack/types@11.1.0
-
d980f0d: feat: add a first-class
userfield type (person picker)A new
userfield type — the equivalent of Airtable's Collaborator / Notion's Person / Salesforce'sLookup(User). Authored asField.user({ ... }); use{ multiple: true }for collaborators/watchers and{ defaultValue: 'current_user' }to auto-fill the acting user on create.Why a distinct type rather than telling authors to
Field.lookup('sys_user'): selecting a person is table-stakes, but the value is in modelling discoverability — a "User" entry in the Studio/AI field palette instead of requiring authors (and AI) to know to reference the internalsys_usersystem object — pluscurrent_userdefaults and a user-search picker. Storage and runtime are unchanged.Deliberately NOT a new storage primitive.
useris a semantic specialization oflookupwith the target fixed tosys_user: it shares the exact lookup code path — same FK string column (multiple⇒ JSON), same$expandresolution, same indexing — so referential integrity and fresh display names come for free, and nothing is re-implemented. An existingField.lookup('sys_user')is therefore equivalent at the storage layer (zero data migration to adoptField.user).Ownership semantics are unchanged: the existing
owner_idconvention +plugin-securityauto-stamp/RLS still apply. A declarativeownerflag is a possible future follow-up; intentionally not added here to avoid a second field type for what is a system role (rationale: keep theFieldTypesurface lean — see related ADR-0059 freeze discipline).Changes:
FieldTypegains'user'+Field.user()builder; the SQL/Mongo drivers treatuserexactly likelookup; the engine resolves$expandforuserfields and honours a newdefaultValue: 'current_user'token (resolved app-side from the execution context, mirroring theNOW()convention); kanban group-by and symbolic seed references acceptuser; approvals enrichuserreferences. The public API surface is unchanged (additive enum member).
-
98a1535: Fix: store SQLite
created_at/updated_atin one canonical, timezone-explicit format (ADR-0074)The two SQLite write paths disagreed on the audit-timestamp format. INSERT fell back to the column default
CURRENT_TIMESTAMP('YYYY-MM-DD HH:MM:SS') while UPDATE stampedtoISOString().replace('T',' ').replace('Z','')('YYYY-MM-DD HH:MM:SS.mmm') — both timezone-naive, space-separated strings thatDate.parsereads as local time. On a non-UTC runtime a stored UTC wall-clock silently shifted by the host offset; e.g. the objectos kernel freshness probe compared a shiftedupdated_atagainst an absolutebuiltAtMsand never evicted (publishes/installs/config toggles didn't take effect until the LRU TTL expired).create/bulkCreate/upsert/updatenow stamp a single canonical ISO-8601 instant with an explicitZ(new Date().toISOString()) — matching the caller-stamped paths (sys_metadata, the service outboxes) and Postgres/MySQL's nativenow(). Because the stamp is applied app-side (not via the column default), existing tenant databases are fixed immediately, not just freshly created tables.formatOutputadditionally repairs any legacy/raw zone-naive audit timestamp to the same format on read (idempotent), so old rows read back unambiguously without a data migration.upsertnow treatscreated_atas insert-only — a conflicting merge never overwrites it.Postgres/MySQL are unaffected (they store a real zone-aware
TIMESTAMP). -
bc22a89: Fix: present
Field.timeas a wall-clock time-of-day on read (SQLite)Field.timeis a tz-naive time-of-day, not an instant (#2004). AdefaultValue: 'NOW()'time column historically took the full SQLiteCURRENT_TIMESTAMPdefault, so a defaulted/legacy row read back a full'YYYY-MM-DD HH:MM:SS'timestamp instead of a time-of-day.formatOutputnow repairs aField.timevalue to just its time portion (toTimeOnly): a legacy full timestamp — or a full ISO value that leaked into the column — is sliced toHH:MM[:SS[.fff]], while a value already stored as a bare time-of-day is left untouched. This is a deliberately NARROW, read-only normalization with no write/filter counterpart, so it introduces no write/read asymmetry and preserves exact round-trips for bare time-of-day values (e.g. the field-zoof_timeguard). Runs for every dialect (a native TIME column already returns a time-of-day, so it is a no-op there).Completes the temporal-field read normalization alongside #2346:
datetimefolds to a canonical ISO-8601-Zinstant,datetoYYYY-MM-DD, andtimeto a wall-clock time-of-day. -
8a7e9f1: Fix: canonical storage + presentation for user-declared
NOW()-default temporal fields on SQLite (ADR-0074 follow-up)A user-declared
Field.datetime(ordate/time) withdefaultValue: 'NOW()'took theknex.fn.now()→CURRENT_TIMESTAMPcolumn default on SQLite, storing a timezone-naive, space-separated'YYYY-MM-DD HH:MM:SS'(no millis, no zone).Date.parsereads such a zone-less string as local time, so the stored UTC wall-clock shifted by the host offset on a non-UTC runtime — the same class of bug ADR-0074 fixed for the builtincreated_at/updated_ataudit columns, but left scoped out for user fields. Worse, the same column mixed storage: an explicit JSDateis bound by better-sqlite3 as INTEGER epoch ms, while an omitted value took the naive TEXT default — so one column held both INTEGER ms and naive TEXT.This fix, SQLite-only:
- DDL default → canonical. The
NOW()default now emits a per-type canonical viastrftime: datetime → ISO-8601 with explicitZ(strftime('%Y-%m-%dT%H:%M:%fZ','now'), e.g.2026-06-26T10:34:13.891Z, matchingnew Date().toISOString()); date →YYYY-MM-DD; time →HH:MM:SS.ffftime-of-day (not a full timestamp). - Read → uniform instant.
formatOutputfolds everyField.datetimestorage form — INTEGER epoch ms, canonical ISO-Z, and legacy naiveCURRENT_TIMESTAMPTEXT — to one canonical ISO-8601-Zinstant (normalizeSqliteDatetimeOutput), interpreting a naive wall-clock as UTC. Idempotent on already-zone-explicit values; total on null/unparseable. This transparently repairs existing rows on read (a DDL default only governs newly-created columns), so no data migration is needed — mirroring theField.date/numeric read-repairs already in place.
Applied as DDL-default + read-normalization, NOT app-side write stamping (the inverse of ADR-0074's audit-column fix): the read path already repairs existing-table rows transparently, and an explicit
Dateis bound as INTEGER epoch ms regardless of any write stamp, so stamping wouldn't make on-disk storage uniform anyway — the INTEGER-vs-TEXT split is inherent to SQLite and resolved at the read boundary. This keeps the hot insert/upsert/bulk paths untouched.The analytics SQL-bucketing path (
strftime, bypassesformatOutput) is unchanged: ISO-ZTEXT buckets identically to the old naive TEXT. Postgres/MySQL keep nativenow()(a real zone-awareTIMESTAMP) and are entirely unaffected.Generalizes ADR-0074's
repairNaiveUtcAuditTimestampby also folding the INTEGER epoch-ms storage form; the two read-repairs can be unified once both land. - DDL default → canonical. The
-
Updated dependencies [ab5718a]
-
Updated dependencies [4845c12]
-
Updated dependencies [c1a754a]
-
Updated dependencies [6fbe91f]
-
Updated dependencies [715d667]
-
Updated dependencies [5eef4cf]
-
Updated dependencies [72759e1]
-
Updated dependencies [6c4fbd9]
-
Updated dependencies [ef3ed67]
-
Updated dependencies [cd51229]
-
Updated dependencies [7697a0e]
-
Updated dependencies [e7e04f1]
-
Updated dependencies [cfd5ac4]
-
Updated dependencies [2be5c1f]
-
Updated dependencies [ad143ce]
-
Updated dependencies [5c4a8c8]
-
Updated dependencies [3afaeed]
-
Updated dependencies [795b6d1]
-
Updated dependencies [8801c02]
-
Updated dependencies [3d04e06]
-
Updated dependencies [4a84c98]
-
Updated dependencies [c715d25]
-
Updated dependencies [aa33b02]
-
Updated dependencies [d980f0d]
-
Updated dependencies [a658523]
-
Updated dependencies [82ff91c]
-
Updated dependencies [638f472]
- @objectstack/spec@11.0.0
- @objectstack/types@11.0.0
- @objectstack/core@11.0.0
-
5ba52b0: fix(driver-sql): honor
tenancy.enabled:falsein driver org-scopingThe driver auto-detects
organization_idas a tenant-isolation column and, when the caller passesDriverOptions.tenantId, scopes reads/updates/deletes to that tenant (and injects the column on inserts). The implicit column-detection fallback ignored an explicittenancy.enabled === false, so a platform-global object that opts out of tenancy but carries an optionalorganization_idFK (e.g.sys_license) was still org-scoped — an authenticated caller's active-orgtenantIdthen hid every NULL-org / cross-org row. The opt-out is now honored in a single sharedcomputeTenantField()used by bothinitObjectsandregisterExternalObject(which had drifted). CoversTursoDriver(extendsSqlDriver). Genuine org-scoped objects are unaffected.- @objectstack/spec@10.3.0
- @objectstack/core@10.3.0
- Updated dependencies [b496498]
- @objectstack/spec@10.2.0
- @objectstack/core@10.2.0
-
517dad9: Schema drift detection +
os migratefor non-additive metadata changes (#2186).The metadata→DB schema sync was additive-only: it created tables and added columns but never altered/dropped existing ones, so relaxing
required, changing a type/length, or dropping a field silently diverged from an existing database. The physical column won at write time, surfacing a misleadingorganization_id is required400 even though/metareported the field optional.- driver-sql — the SQL driver now detects managed-schema drift (metadata is
the source of truth) and categorises each divergence
safe/needs_confirm/destructive.initObjectswarns once per divergence with an actionable hint. A new opt-inSqlDriverConfig.autoMigrate: 'safe'auto-applies the loosening subset (relaxNOT NULL, widen varchar) so an existing dev DB self-heals on restart — never destructive, force-disabled underNODE_ENV=production. New public methodsdetectManagedDrift()/applyMigrationEntries(). SQLite reconciles via the official table-rebuild (copy → swap), preserving data; Postgres/MySQL alter in place. - cli — new
os migrate plan(dry-run, categorised diff) andos migrate apply(--allow-destructivefor drops/tightenings, confirm gate,--json).os dev/servenow passautoMigrate: 'safe'in dev only. - rest — a
NOT NULLviolation that reaches the driver (metadata validation already passed) now carries a drift-awarehintpointing atos migrate, instead of only the misleading "field is required" message. TheVALIDATION_FAILED/fieldsenvelope is unchanged for back-compat.
- driver-sql — the SQL driver now detects managed-schema drift (metadata is
the source of truth) and categorises each divergence
- Updated dependencies [49da36e]
- Updated dependencies [ac79f16]
- @objectstack/spec@10.1.0
- @objectstack/core@10.1.0
-
92db3e5: feat(driver-sql): honor
external.columnMapon federated (external) objects (ADR-0015).When a federated object declares
external.columnMap({ remoteColumn -> localField }), the SQL driver now translates queries to the physical remote columns: WHERE and ORDER BY map local fields to remote columns (value coercion stays keyed by the local field),formatOutputrenames remote-column keys back to local field names on read, and write payloads are key-remapped. Managed objects and external objects without a columnMap are unchanged (the resolver falls back to the existing per-site behavior). -
2a1b16b: fix(ADR-0015): honor
external.remoteName/external.remoteSchemaon the federation read path.The query path previously resolved an external object's physical table from the object name, ignoring its
externalbinding — so a federated object bound to a differently-named remote table failed withno such table, and ADR-0015's ownwh_order→mart.fact_ordersexample was unqueryable. The SQL driver now resolves the remote table (remoteName, plusremoteSchemavia.withSchema()on pg/mysql) and registers external objects' read-coercion metadata without DDL (SqlDriver.registerExternalObject, routed from the engine/plugin schema-sync). The managed path is unchanged. See ADR-0015 §18. -
Updated dependencies [d7ff626]
-
Updated dependencies [2a1b16b]
-
Updated dependencies [e16f2a8]
-
Updated dependencies [e411a82]
-
Updated dependencies [a581385]
-
Updated dependencies [d5f6d29]
-
Updated dependencies [220ce5b]
-
Updated dependencies [3efe334]
-
Updated dependencies [feead7e]
-
Updated dependencies [6ca20b3]
-
Updated dependencies [5f875fe]
-
Updated dependencies [b469950]
- @objectstack/spec@10.0.0
- @objectstack/core@10.0.0
-
36138c7: feat(autonumber): date, {field} and per-scope counter reset for autonumber formats
autonumberFormatpreviously only understood a single{0000}sequence slot — everything else was a fixed literal prefix on one global counter. Real MES/eHR record numbers need three more token classes, so the format is now tokenized by a shared pure renderer in@objectstack/spec(parseAutonumberFormat/renderAutonumber) that the engine fallback and the SQL driver both call, so they emit byte-identical numbers (#1603 parity):- Date tokens —
{YYYY}{YY}{MM}{DD}{YYYYMMDD}resolve the calendar day in the request's business timezone (ExecutionContext.timezone, ADR-0053; UTC fallback), threaded through the newDriverOptions.timezone. {field}interpolation —{section}{island_zone}{000}substitutes record field values into the prefix.- Per-scope counter reset — the counter's scope is the rendered prefix before
the sequence slot, so
AD{YYYYMMDD}{0000}resets daily,{section}{island_zone}{000}numbers per group, and{plan_no}{000}numbers per parent — all from one mechanism, no separate reset config.
Fixed-prefix formats like
CASE-{0000}render an empty scope and keep their single global counter, so existing sequences are unchanged. The persistent_objectstack_sequencestable is keyed by akey_hash(SHA-256 ofobject, tenant_id, field, scope) — a single 64-char primary key that keys every dialect uniformly, stays within MySQL's utf8mb4 index-length limit (four raw columns would not), and letsscopebe a generous non-indexed column. Deployments with an older table (3-column, or an interimscopecolumn) are migrated in place on first use, carrying existing counters toscope=''.Guardrails:
- Empty interpolated field is a hard error, not a silent mis-number. A
{field}token whose value is missing at create time would render to an empty prefix and collapse the record into the wrong counter scope. Both the SQL driver and the engine fallback now refuse to generate and throw a clear error naming the empty field (sharedmissingFieldValueshelper). - Build-time lint (
@objectstack/cli compile).autonumberformats are checked against the object's fields: a{field}token naming a non-existent field (or the autonumber field itself) fails the build; a token naming an optional field emits an advisory warning to mark itrequired: true. - Migration fails safe. If a legacy table cannot be migrated to the
key_hashshape, fixed-prefix sequences keep working via the legacy key and a per-scope write raises an actionable error instead of corrupting counters. - Long
{field}scopes are supported (e.g. a long{plan_no}): the non-indexedscopecolumn and hashed key remove the old varchar/PK length ceiling.
Notes on inherent semantics (documented, not bugs):
- The counter scope IS the rendered prefix. When two records' tokens render to the
same prefix string (e.g.
{a}{b}for('AB','C')and('A','BC')) they also render the same visible number, so they share one counter to stay unique — the remedy for genuinely-distinct groups is an unambiguous format (a delimiter literal between variable tokens). - The sequence pad width is a MINIMUM; past it the number grows (
{000}→1000), it never wraps — matching mainstream autonumber semantics.
- Date tokens —
- Updated dependencies [e7f6539]
- Updated dependencies [2365d07]
- Updated dependencies [6595b53]
- Updated dependencies [fa8964d]
- Updated dependencies [36138c7]
- Updated dependencies [a8e4f3b]
- Updated dependencies [4c213c2]
- Updated dependencies [2afb612]
- @objectstack/spec@9.11.0
- @objectstack/core@9.11.0
-
db02bd5: Fix dashboard time-series charts / "last N months" KPIs that filter or group by a
Field.datetimecolumn silently returning "No rows".The analytics
NativeSQLStrategycompiles dashboard relative-date tokens ({12_months_ago},{today}, …) to ISO date strings and binds them directly into raw SQL, bypassing the driver's own filter coercion. Under better-sqlite3 aField.datetimecolumn is stored as an INTEGER epoch (ms), soassessed_at >= '2025-06-18'became a TEXT-vs-INTEGER affinity compare that is always false — an empty result even though the rows exist.Field.datecolumns store ISO TEXT and were unaffected.The strategy now coerces a temporal comparand to the column's on-disk storage form via a new optional
StrategyContext.coerceTemporalFilterValuehook, wired to the driver's publicSqlDriver.temporalFilterValue(the single source of truth for the storage convention). Coercion is dialect-correct: SQLiteField.datetime→ epoch ms;Field.datetext and native-timestamp dialects (Postgres/MySQL) are left unchanged, so Postgres is never handed an epoch integer. Applied togte/lte/gt/lt/equals,in/notIn, and thedateRange/timeDimensionBETWEENpath. -
d9508d1: fix(driver-sql): make numeric-scalar type fidelity self-heal on legacy SQLite columns
The #2025 fix mapped
rating/slider/progressto numeric columns, but SQLite never alters a column's type in place and the schema reconciler only adds missing columns — so a column created before that fix keeps its TEXT affinity and would still read back'4'instead of4forever.A read-side numeric coercion (the new
numericFieldsregistry, single-sourced fromNUMERIC_SCALAR_TYPES) now coerces numeric-looking stored strings back to numbers on read, mirroring howdateFieldsalready repairs legacy timestamp-typedField.daterows. The fidelity no longer depends on column affinity alone;nulland genuinely non-numeric legacy values are left intact rather than turned into0/NaN. -
1d352d3: fix(driver-sql): round-trip rating/slider/toggle/progress with type fidelity
rating/slider/toggle/progresshad no case in the DDL column-type switch, so they fell todefault → table.string(TEXT affinity). SQLite then coerced the written value to a string —rating: 4read back'4',toggle: trueread back'1'— so the value persisted but the JS type leaked on read. On a low-code platform where field types are author-driven, a field that silently returns the wrong type is a runtime-fidelity trap the static gates and value-loss tests don't catch.rating/slider/progressnow map to a REAL (numeric) column.togglemaps to a boolean column and is registered in the boolean read-coercion path, so stored1/0come back as real JS booleans.- The object-valued
record/video/audiotypes are folded into the sharedJSON_COLUMN_TYPESsource, and the DDLdefaultcase now derives JSON-vs-string from that set, so the column-type switch andisJsonField(the read-side deserializer) can no longer drift.
-
Updated dependencies [db02bd5]
-
Updated dependencies [641675d]
-
Updated dependencies [94e9040]
-
Updated dependencies [1f88fd9]
-
Updated dependencies [1f88fd9]
- @objectstack/spec@9.10.0
- @objectstack/core@9.10.0
- @objectstack/spec@9.9.1
- @objectstack/core@9.9.1
-
bfa3102: fix: array-valued field types persist, and
Field.timeaccepts time-of-day — two field-type runtime gaps found driving the showcase field-zoo (which had no seed data, so neither was ever exercised).Array/object fields broke every write (driver-sql).
multiselect/checkboxes/tags/repeater/vectorwere absent from the SQL driver's JSON-field classification, so their array values reached the better-sqlite3 binder un-serialized and threw "SQLite3 can only bind numbers, strings, bigints, buffers, and null" — a 500 on insert/update for common field types (eventask.labelson a normal object). The DDL column-type switch andisJsonFieldhad drifted into two separate lists; they now share oneJSON_COLUMN_TYPESsource that includes the array/object types, so these columns are created as JSON and round-trip as arrays/objects. AformatInputsafety net additionally serializes any stray array/object value so an unclassified field degrades to a stored string instead of crashing.Field.timerejected every valid value (objectql). The validator reused the date/datetime branch (Date.parse), which isNaNfor any bare time string — so atimefield could never accept14:30or09:05:30.timenow validates a time-of-day (HH:MM/HH:MM:SS, optional fractional seconds andZ/offset) and still accepts a full ISO datetime;date/datetimeare unchanged.Verified live on app-showcase: the full field-zoo specimen (all input-able field types) now persists and round-trips. Regression tests added for both.
-
796f0d6: fix(driver-sql):
Field.dateis now stored and returned as a tz-naiveYYYY-MM-DDcalendar day (ADR-0053 Phase 1)A
Field.date("close date", "due date", "birthday") is semantically a timezone-naive calendar day, but the SQL driver was treating it as an instant:formatInputwrote the value verbatim (keeping any time component, sodev.dbheldclose_date = "2026-07-15T17:24:56.533Z"), while the filter layer (coerceFilterValue) already normalized the comparand to date-onlyYYYY-MM-DD. That write/filter asymmetry meant a date-equality filter —close_date == '2026-07-15',expires_on: { $in: [...] }, or adaysFromNow(n)-style comparand — compared"2026-07-15T17:24Z"against"2026-07-15"and silently matched nothing.This patch aligns the write/read boundary with the date-only contract the filter already enforced:
- Write (
formatInput): everyField.datevalue (a JSDate, a full-ISO string, or an already date-only string) collapses toYYYY-MM-DDbefore insert/update. ADatecollapses to its UTC calendar day, matchingcoerceFilterValue. - Read (
formatOutput):Field.datevalues are returned asYYYY-MM-DD, slicing any stored time component. This transparently repairs legacy rows that were written as a full timestamp, so date-equality works without a data migration. Read normalization now runs on thefindpath for every dialect (previously onlyfindOne), matching the new behaviour. - The truncation logic is shared by the filter, write and read paths via a single
toDateOnlyhelper, so all three agree on what a date is.
Field.datetimeis unchanged — it keeps full-instant (UTC millisecond) semantics.Out of scope (ADR-0053 Phase 2): timezone-aware
today()/daysFromNow()/daysAgo(), an org/user reference timezone, anddatetimerender-time TZ. See ADR-0053 and issue #1928. - Write (
-
Updated dependencies [84249a4]
-
Updated dependencies [11af299]
-
Updated dependencies [d5774b5]
-
Updated dependencies [134043a]
-
Updated dependencies [90108e0]
-
Updated dependencies [9afeb2d]
-
Updated dependencies [6bec07e]
-
Updated dependencies [601cc11]
-
Updated dependencies [575448d]
- @objectstack/spec@9.9.0
- @objectstack/core@9.9.0
- Updated dependencies [97c55b3]
- Updated dependencies [1b1f490]
- @objectstack/spec@9.8.0
- @objectstack/core@9.8.0
- @objectstack/spec@9.7.0
- @objectstack/core@9.7.0
- Updated dependencies [d1e930a]
- Updated dependencies [71578f2]
- Updated dependencies [5e3a301]
- Updated dependencies [5db2742]
- @objectstack/spec@9.6.0
- @objectstack/core@9.6.0
- Updated dependencies [ee72aae]
- @objectstack/spec@9.5.1
- @objectstack/core@9.5.1
- Updated dependencies [d08551c]
- Updated dependencies [707aeed]
- Updated dependencies [7a103d4]
- Updated dependencies [4b01250]
- @objectstack/spec@9.5.0
- @objectstack/core@9.5.0
-
b678d8c: fix(driver-sql): an unknown
$selectcolumn must not zero the result setfind()swallowed any "no such column" error into an empty array. A projected$selectnaming a column the table lacks (e.g. a generic list view auto-requestingstatus/due_date/imageon an object without them) then made the WHOLE query return zero rows — reading to the UI as "no records exist" while the data was actually there: a silent data-loss footgun.When the failure comes from the projection, retry once with
SELECT *so the real rows still come back (the phantom field is simply absent from each row). Non-projection errors (unknown table, etc.) still surface as before. This driver backstop holds even when the engine's unknown-field filter cannot fire because the object's schema is not populated in the registry (notably the cloud multi-tenant runtime). -
Updated dependencies [060467a]
-
Updated dependencies [0856476]
-
Updated dependencies [b678d8c]
-
Updated dependencies [b678d8c]
-
Updated dependencies [b678d8c]
- @objectstack/spec@9.4.0
- @objectstack/core@9.4.0
- Updated dependencies [1ada658]
- Updated dependencies [3219191]
- Updated dependencies [290f631]
- Updated dependencies [50b7b47]
- Updated dependencies [f15d6f6]
- Updated dependencies [f8684ea]
- Updated dependencies [b4765be]
- @objectstack/spec@9.3.0
- @objectstack/core@9.3.0
- Updated dependencies [2f57b75]
- Updated dependencies [2f57b75]
- @objectstack/spec@9.2.0
- @objectstack/core@9.2.0
- Updated dependencies [b9062c9]
- @objectstack/spec@9.1.0
- @objectstack/core@9.1.0
- Updated dependencies [1817845]
- @objectstack/spec@9.0.1
- @objectstack/core@9.0.1
- Updated dependencies [4c3f693]
- Updated dependencies [0bf39f1]
- Updated dependencies [f533f42]
- Updated dependencies [1c83ee8]
- @objectstack/spec@9.0.0
- @objectstack/core@9.0.0
- @objectstack/spec@8.0.1
- @objectstack/core@8.0.1
-
b990b89: fix(autonumber): one owner for autonumber generation — the persistent driver sequence (#1603)
Autonumber values were generated in TWO places: the SQL driver's persistent, atomic
_objectstack_sequencestable AND a non-persistent in-memory counter in the ObjectQL engine. Because the engine pre-filled the field BEFORE calling the driver, the driver always saw a value already set and skipped — so the persistent sequence was effectively dead code, and a multi-instance / post-restart deployment could mint duplicate numbers from the in-memory counter.This makes generation single-owner:
-
@objectstack/spec—DriverCapabilitiesgains an optionalautonumberflag: "driver natively generates persistent autonumber/sequence values". -
@objectstack/driver-sql— advertisessupports.autonumber = true.bulkCreate()now fills autonumber fields too (previously onlycreate()/upsert()did), so bulk inserts also draw from the persistent sequence. Field parsing now honors either the spec-canonicalautonumberFormatkey OR theformatshorthand (both appear in metadata). -
@objectstack/objectql— when the driver advertises native autonumber support, the engine NO LONGER pre-fills (it defers entirely to the persistent driver sequence as the single source of truth). For drivers without native support (memory, mongodb) the in-memory fallback is unchanged. The fallback also now reads eitherautonumberFormatorformat. Record-validation exemptsautonumberfields from therequiredcheck — the value is runtime-owned and assigned after validation, so a required record number is never rejected as "missing".
No metadata changes required. Existing data is respected: the driver bootstraps each sequence from the current max numeric tail on first use.
-
-
1e8b680: fix(security): close four P0 launch-readiness findings
- plugin-auth (P0-1):
generateSecret()now throws (fails boot) when noOS_AUTH_SECRETis set andNODE_ENV==='production', instead of silently falling back to a predictabledev-secret-<timestamp>(session forgery). The dev/test fallback is unchanged. - plugin-security (P0-2): the permission-resolution
catchnow fails closed — it logs at ERROR and throwsPermissionDeniedErrorrather thanreturn next(). A degraded metadata service can no longer let every authenticated request bypass RBAC/RLS. System operations still bypass as before. - driver-sql (P0-3): the
contains/$containsoperator now escapes LIKE metacharacters (%/_/\) in the user value and binds an explicitESCAPE '\', so a value of%matches literally instead of every row (filter bypass). Correct across SQLite/MySQL/Postgres. - driver-mongodb (P0-4): the field-operator translator now rejects unknown
$-operators instead of passing them through, blocking$where/$function/$expr(server-side JS execution / query-intent bypass). All legitimate ObjectQL operators remain allowlisted.
+12 regression tests across the four packages.
- plugin-auth (P0-1):
-
Updated dependencies [a46c017]
-
Updated dependencies [b990b89]
-
Updated dependencies [99111ec]
-
Updated dependencies [d5a8161]
-
Updated dependencies [5cf1f1b]
-
Updated dependencies [9ef89d4]
-
Updated dependencies [3306d2f]
-
Updated dependencies [c262301]
-
Updated dependencies [bc44195]
-
Updated dependencies [9e2e229]
- @objectstack/spec@8.0.0
- @objectstack/core@8.0.0
- @objectstack/spec@7.9.0
- @objectstack/core@7.9.0
- Updated dependencies [06f2bbb]
- Updated dependencies [36719db]
- Updated dependencies [424ab26]
- @objectstack/spec@7.8.0
- @objectstack/core@7.8.0
-
764c747: fix(metadata): home the metadata-storage objects in metadata-core and register them from ObjectQL
Standalone "host config" apps boot without
@objectstack/metadata's MetadataPlugin, so nobody registered the metadata-storage objects (sys_metadata,_history,_audit,sys_view_definition) into ObjectQL — their tables were never schema-synced and ObjectQL's own protocol (loadMetaFromDb/getMetaItems) failed withno such table: sys_metadataon every read.- Move the four storage-object definitions from
@objectstack/platform-objects/metadatato@objectstack/metadata-core(the lowest package shared by their real consumers);platform-objects/metadatanow re-exports them for back-compat. ObjectQLPluginregisters these objects itself (gated onenvironmentId === undefined, mirroringrestoreMetadataFromDb) so their tables always sync on platform/standalone kernels.- Gate the SQL driver's tenant-audit warning on actual multi-tenant mode —
organization_idnow exists on every table, so column presence alone no longer implies "tenant-scoped"; single-tenant boots no longer spam the warning for system writes.
- Move the four storage-object definitions from
-
Updated dependencies [b391955]
-
Updated dependencies [f06b64e]
-
Updated dependencies [023bf93]
- @objectstack/spec@7.7.0
- @objectstack/core@7.7.0
- Updated dependencies [955d4c8]
- Updated dependencies [c4a4cbd]
- Updated dependencies [b046ec2]
- Updated dependencies [2170ad9]
- Updated dependencies [02d6359]
- Updated dependencies [7648242]
- Updated dependencies [8fa1e7f]
- Updated dependencies [55866f5]
- Updated dependencies [60f9c45]
- @objectstack/spec@7.6.0
- @objectstack/core@7.6.0
- @objectstack/spec@7.5.0
- @objectstack/core@7.5.0
- @objectstack/spec@7.4.1
- @objectstack/core@7.4.1
-
24c9013: fix(driver-sql): materialize declared object-level indexes (#1459)
The SQL driver synced columns and field-level
unique, but silently dropped object-level declaredindexes(ObjectSchema.indexes: [{ fields, unique }]). As a result several documented multi-column UNIQUE / dedup guarantees were never enforced at the DB level — a freshdev --freshsqlite DB showed only primary-key autoindexes.initObjectsnow materializes declared indexes (syncDeclaredIndexes) after the table is created/altered:- single- and multi-column indexes, including
UNIQUE - NULL-distinct semantics (the cross-dialect default), so multiple NULL rows stay insertable while non-NULL duplicates are rejected — matching the convergence-on-conflict pattern the messaging pipeline relies on
- idempotent: deterministic, length-bounded index names + per-dialect existing-index introspection (sqlite/pg/mysql); "already exists" races are absorbed
- indexes referencing a non-materialized (virtual
formula) column are skipped with a warning instead of failing sync
The
indexesdriver capability flag is nowtrue. - single- and multi-column indexes, including
-
2faf9f2: External Datasource Federation (ADR-0015) — Phase 1.
Adds the spec foundation and the DDL gate for federating mature external databases without ObjectStack ever mutating their schema:
Datasource.schemaMode(managed|external|validate-only) andDatasource.externalsettings, with a cross-field invariant.Object.externalbinding (remote table/schema, writability, column map).- Shared error contract:
ExternalSchemaMismatchError,ExternalWriteForbiddenError,ExternalSchemaModeViolationError(stablecodes) + structuredSchemaDiffEntryrendering. driver-sqlDDL gate: schema-mutating DDL (initObjects/syncSchema/dropTable) is rejected whenschemaMode !== 'managed'.
All changes are additive and backward-compatible (
schemaModedefaults to'managed').
- Updated dependencies [23c7107]
- Updated dependencies [c72daad]
- Updated dependencies [f115182]
- Updated dependencies [2faf9f2]
- Updated dependencies [2faf9f2]
- Updated dependencies [2faf9f2]
- Updated dependencies [58b450b]
- Updated dependencies [82eb6cf]
- Updated dependencies [13d8653]
- Updated dependencies [ff3d006]
- Updated dependencies [5e831de]
- @objectstack/spec@7.4.0
- @objectstack/core@7.4.0
- Updated dependencies [5e7c554]
- @objectstack/spec@7.3.0
- @objectstack/core@7.3.0
- @objectstack/spec@7.2.1
- @objectstack/core@7.2.1
- @objectstack/spec@7.2.0
- @objectstack/core@7.2.0
- Updated dependencies [47a92f4]
- @objectstack/spec@7.1.0
- @objectstack/core@7.1.0
- Updated dependencies [74470ad]
- Updated dependencies [d29617e]
- Updated dependencies [dc72172]
- @objectstack/spec@7.0.0
- @objectstack/core@7.0.0
- @objectstack/spec@6.9.0
- @objectstack/core@6.9.0
- @objectstack/spec@6.8.1
- @objectstack/core@6.8.1
- Updated dependencies [6e88f77]
- Updated dependencies [c8b9f57]
- @objectstack/spec@6.8.0
- @objectstack/core@6.8.0
- @objectstack/spec@6.7.1
- @objectstack/core@6.7.1
-
4944f3a: Promote native database client packages so npm consumers can boot without manual installs.
better-sqlite3is now anoptionalDependency(prebuilt binaries cover the common case), sonpx @objectstack/cli startboots a default SQLite database out-of-the-box.pg,mysql2,sqlite3, andtediousare declared as optionalpeerDependencies(peerDependenciesMeta.optional = true), removing install warnings while keeping the loader-on-demand pattern.
Fixes:
Knex: Cannot find module 'better-sqlite3'on freshnpm install @objectstack/clifollowed byobjectstack start. -
Updated dependencies [430067b]
-
Updated dependencies [4f9e9d4]
- @objectstack/spec@6.7.0
- @objectstack/core@6.7.0
- Updated dependencies [a49cfc2]
- @objectstack/spec@6.6.0
- @objectstack/core@6.6.0
- @objectstack/spec@6.5.1
- @objectstack/core@6.5.1
- @objectstack/spec@6.5.0
- @objectstack/core@6.5.0
- Updated dependencies [f8651cc]
- Updated dependencies [f8651cc]
- Updated dependencies [0bf6f9a]
- @objectstack/spec@6.4.0
- @objectstack/core@6.4.0
- @objectstack/spec@6.3.0
- @objectstack/core@6.3.0
- Updated dependencies [b4c74a9]
- @objectstack/spec@6.2.0
- @objectstack/core@6.2.0
- @objectstack/spec@6.1.1
- @objectstack/core@6.1.1
- Updated dependencies [93c0589]
- @objectstack/spec@6.1.0
- @objectstack/core@6.1.0
- Updated dependencies [629a716]
- Updated dependencies [dbc4f7d]
- Updated dependencies [944f187]
- @objectstack/spec@6.0.0
- @objectstack/core@6.0.0
- Updated dependencies [bab2b20]
- Updated dependencies [fa011d8]
- Updated dependencies [b806f58]
- @objectstack/spec@5.2.0
- @objectstack/core@5.2.0
- Updated dependencies [75f4ee6]
- Updated dependencies [823d559]
- @objectstack/spec@5.1.0
- @objectstack/core@5.1.0
- Updated dependencies [2f9073a]
- @objectstack/spec@5.0.0
- @objectstack/core@5.0.0
- Updated dependencies [2869891]
- @objectstack/spec@4.2.0
- @objectstack/core@4.2.0
- @objectstack/spec@4.1.1
- @objectstack/core@4.1.1
-
0cc0374: feat(driver-sql): tenant-isolated auto_number sequences backed by a persistent counter table
Breaking nothing; new behaviour is opt-in via object schema.
The SQL driver now generates auto_number / autonumber field values via a dedicated
_objectstack_sequencestable keyed by(object, tenant_id, field)instead of scanning the data table for the current MAX on every insert.Highlights:
- Tenant isolation. Objects with an
organization_idfield get a separate counter per organization. Two tenants creating contracts at the same time both legitimately observeCTR-0001,CTR-0002, … in their own namespaces — they no longer interleave or skip numbers. - Tenant resolution. Source order:
row[organization_id]→DriverOptions.tenantId→__global__sentinel for org-less objects (e.g. setup-side singletons share one counter). - Bootstrap from existing data. On the first reservation in a new
(object, tenant, field)tuple, the driver seedslast_valuefrom the current per-tenant MAX so legacy/seeded records keep their position and downstream inserts pick up monotonically (gaps are tolerated). - Atomic increment. Each reservation runs in a transaction with
SELECT … FOR UPDATE(where the dialect supports it) and a singleUPDATEoflast_value. Tested with 25 concurrent inserts in one tenant producing 25 distinct sequence values. - Caller overrides honoured. A row that already has an explicit value for the auto_number field is left untouched, and the sequence bootstrap respects that value so future reservations advance past it.
- Dual spelling. Both
type: 'auto_number'(snake) andtype: 'autonumber'(the spec factory output) are recognised.
Migration notes:
- The first time the driver handles an auto_number insert, it creates
the
_objectstack_sequencestable automatically — no manual DDL. - Pre-existing data is not renumbered. Gaps introduced by older
cross-tenant logic (where a tenant's number could "jump" because it
inherited another tenant's MAX) remain in place; subsequent inserts
continue from
MAX + 1in the affected tenant.
- Tenant isolation. Objects with an
-
5b878d9: Generate
auto_number/autonumberfield values on insert. The driver parses the field'sformattemplate (e.g.CTR-{0000}) to extract the prefix and pad-width, then scans existing rows with the same prefix and emitsprefix + padded(maxN + 1)for any row that omits the field.Note: per-call MAX+1 — not atomic across concurrent writers. Fine for seed-data and low-write demo loads; production deployments should layer a dedicated sequence table.
-
f0b3972: Driver-level tenant isolation for objects with
organization_id.SqlDrivernow auto-applies aWHERE organization_id = :tenantIdpredicate on every read/update/delete and auto-injects the column on insert when the caller passesoptions.tenantIdand the object schema declares anorganization_idfield.bulkCreate,bulkDelete,updateMany,deleteMany,countandaggregateare all scoped.ObjectQL's engine now threads
ExecutionContext.tenantIdinto the driver options for every CRUD entry point (includingexpandRelatedRecords), so a tenant-scoped session can no longer cross tenants — even through lookup expansion or count fallbacks.Backward compatible: callers that omit
tenantId(system tasks, seed scripts) keep getting unscoped behaviour. Explicitorganization_idon an insert row always wins over the contextualtenantIdso admin tooling can still target a specific tenant.13 new tests in
sql-driver-tenant-scope.test.tsverify cross-tenant find/findOne/update/delete/count/bulkCreate/updateMany/deleteMany isolation, the unscoped admin path, and that global objects (noorganization_id) are not scoped. -
0e63f2f: Declarative tenant scoping + audit warn for missing tenantId.
SqlDrivernow readsobj.tenancy.tenantFieldfirst when picking the tenant column for an object, falling back to the implicitorganization_iddetection so legacy objects keep working without a spec migration. Settenancy: { enabled: true, strategy: 'shared', tenantField: 'workspace_id' }on any object to use a custom column.Writes (
create,update,delete,bulkCreate,bulkDelete,updateMany,deleteMany,upsert) that target a tenant-scoped object withoutoptions.tenantIdnow emit one[tenant-audit]warning per{object}:{op}so missing-context bugs surface in CI/logs instead of silently writing globally. The engine auto-silences whenExecutionContext.isSystem === true(boot-time seeds, kernel mirrors). Callers can opt out per-call withoptions.bypassTenantAudit = trueor globally withOS_TENANT_AUDIT=0.Driver README now documents the full scope/bypass matrix and the audit warning.
Three new tests cover the declared-tenant-field path, the audit throttle, and the bypass flag.
- 5683206: Document the tenant-isolation bypass on raw
execute()(bothSqlDriver.execute()andengine.execute()). The behaviour is unchanged —execute()has always passed commands through verbatim — but the JSDoc now spells out the security contract so callers know they must inlineWHERE organization_id = ?themselves or restrict raw execution to genuinely global statements (migrations, control-plane tables). - Updated dependencies [2108c30]
- Updated dependencies [23db640]
- @objectstack/spec@4.1.0
- @objectstack/core@4.1.0
- 15e0df6: chore: unify all package versions to a single patch release
- Updated dependencies [15e0df6]
- @objectstack/spec@4.0.5
- @objectstack/core@4.0.5
- Updated dependencies [326b66b]
- @objectstack/spec@4.0.4
- @objectstack/core@4.0.4
- @objectstack/spec@4.0.3
- @objectstack/core@4.0.3
- 5f659e9: fix ai
- Updated dependencies [5f659e9]
- @objectstack/spec@4.0.2
- @objectstack/core@4.0.2
- Updated dependencies [f08ffc3]
- Updated dependencies [e0b0a78]
- @objectstack/spec@4.0.0
- @objectstack/core@4.0.0
- @objectstack/spec@3.3.1
- @objectstack/core@3.3.1
- 814a6c4: sql driver
- @objectstack/spec@3.3.0
- @objectstack/core@3.3.0