-
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