1717 * own column already provides is NOT a disagreement and must not be
1818 * reported as one — that is the whole content of the 2026-08-25 narrowing,
1919 * and the case that makes the message true again.
20+ *
21+ * ③ **The delivery table speaks the SPEC's vocabulary** (#12131). Its `type`
22+ * is compared with `===` against a declaration's `type`, so a knex builder
23+ * name there is a cross-vocabulary comparison that no declaration can
24+ * satisfy. `id` used to record `'string'` — the `table.string('id')`
25+ * builder name — and reported all 45 correct `id: Field.text(...)`
26+ * declarations in `@objectstack/platform-objects` as disagreements while
27+ * `'string'` was not even authorable. The first case below now holds every
28+ * entry to `FieldType`, so the class fails by name rather than by corpus.
2029 */
2130
2231import { describe , it , expect } from 'vitest' ;
23- import { FieldSchema } from '@objectstack/spec/data' ;
32+ import { FieldSchema , FieldType } from '@objectstack/spec/data' ;
2433import {
2534 FIELD_KEY_STORAGE_CLASS ,
2635 BUILTIN_COLUMN_DELIVERY ,
@@ -60,10 +69,30 @@ describe('the FieldSchema storage/presentation classification (#12015)', () => {
6069 }
6170 } ) ;
6271
72+ it ( '⛔ spells every delivered `type` in the SPEC vocabulary, never a knex builder name (#12131)' , ( ) => {
73+ // The one that got away: `id` recorded `'string'`, the knex builder name,
74+ // and `undeliveredStorageAttributes` compares it with `===` against a
75+ // declaration's `type` — a spec `FieldType`. No declaration could match it
76+ // (`'string'` is absent from FieldType's 49 members and `FieldSchema`
77+ // refuses it), so every correct `id: Field.text(...)` was reported as a
78+ // disagreement: 45 of them on a stock boot of platform-objects.
79+ for ( const [ column , delivery ] of Object . entries ( BUILTIN_COLUMN_DELIVERY ) ) {
80+ expect (
81+ FieldType . options as readonly string [ ] ,
82+ `BUILTIN_COLUMN_DELIVERY.${ column } .type must be a spec FieldType, not a builder name` ,
83+ ) . toContain ( delivery . type ) ;
84+ }
85+ } ) ;
86+
6387 it ( 'records what each builtin column actually delivers, read off the emitting lines' , ( ) => {
64- // `table.string('id').primary()` — varchar(255), NOT NULL, unique, no default.
88+ // `table.string('id').primary()` — varchar(255), NOT NULL, unique, no
89+ // default. varchar canonicalizes to the field type `text`
90+ // (`canonicalizeSqlType('varchar(255)') === 'text'`, pinned in
91+ // `type-compat.test.ts`), so `text` is what this column DELIVERS — which
92+ // is why the platform's own `id: Field.text(...)` declarations agree with
93+ // it exactly (#12131).
6594 expect ( BUILTIN_COLUMN_DELIVERY . id ) . toMatchObject ( {
66- type : 'string ' , maxLength : 255 , unique : true , notNull : true , defaultValue : null ,
95+ type : 'text ' , maxLength : 255 , unique : true , notNull : true , defaultValue : null ,
6796 } ) ;
6897 // `createAuditTimestampColumn` — a timestamp defaulted to the DB clock, left NULLABLE.
6998 for ( const column of [ 'created_at' , 'updated_at' ] ) {
@@ -76,22 +105,30 @@ describe('the FieldSchema storage/presentation classification (#12015)', () => {
76105
77106describe ( 'what a declaration on a builtin column name loses (#12015)' , ( ) => {
78107 it ( 'FIRES on the author error the card was filed for' , ( ) => {
79- // `id: { type: 'number' }` — an author expecting a numeric key.
108+ // `id: { type: 'number' }` — an author expecting a numeric key. This is
109+ // the real author error; ⛔ NOT `{ type: 'text' }`, which is what the
110+ // column delivers (see the silent case below).
80111 expect ( keysOf ( undeliveredStorageAttributes ( 'id' , { type : 'number' } ) ) ) . toEqual ( [ 'type' ] ) ;
81- // The #11456 fixture's shape.
82- expect ( keysOf ( undeliveredStorageAttributes ( 'id' , { type : 'text' , name : 'id' } ) ) ) . toEqual ( [ 'type' ] ) ;
112+ expect ( keysOf ( undeliveredStorageAttributes ( 'id' , { type : 'number' , name : 'id' } ) ) ) . toEqual ( [ 'type' ] ) ;
83113 // …and names what the column really is, not just that something was lost.
84- expect ( undeliveredStorageAttributes ( 'id' , { type : 'text ' } ) [ 0 ] ) . toMatchObject ( {
85- key : 'type' , declared : 'text ' , delivered : 'string ' ,
114+ expect ( undeliveredStorageAttributes ( 'id' , { type : 'number ' } ) [ 0 ] ) . toMatchObject ( {
115+ key : 'type' , declared : 'number ' , delivered : 'text ' ,
86116 } ) ;
87117 } ) ;
88118
89119 it ( 'is SILENT for a presentation-only declaration — the platform honours that half' , ( ) => {
90120 // `sys_presence.id`, verbatim in shape: the population the pre-narrowing
91- // warning was false about.
121+ // warning was false about. ⚠️ It is `Field.text`, and this fixture used to
122+ // spell it `type: 'string'` — matching the delivery table's builder name
123+ // rather than the source. That made this case pass while the same
124+ // declaration as actually written warned (#12131). Verbatim now.
92125 expect (
93- undeliveredStorageAttributes ( 'id' , { type : 'string ' , label : 'Presence ID' , required : true , readonly : true } ) ,
126+ undeliveredStorageAttributes ( 'id' , { type : 'text ' , label : 'Presence ID' , required : true , readonly : true } ) ,
94127 ) . toEqual ( [ ] ) ;
128+ // The #11456 fixture's exact shape — the declaration that started #12015.
129+ // It asks for precisely what the column delivers, so it is SILENT; it was
130+ // reported as a disagreement until the delivery table was corrected.
131+ expect ( undeliveredStorageAttributes ( 'id' , { type : 'text' , name : 'id' } ) ) . toEqual ( [ ] ) ;
95132 expect (
96133 undeliveredStorageAttributes ( 'created_at' , {
97134 type : 'datetime' , label : 'Created At' , defaultValue : 'NOW()' , readonly : true ,
@@ -100,21 +137,21 @@ describe('what a declaration on a builtin column name loses (#12015)', () => {
100137 } ) ;
101138
102139 it ( 'is SILENT for a storage attribute the column already delivers' , ( ) => {
103- expect ( undeliveredStorageAttributes ( 'id' , { type : 'string ' , maxLength : 255 } ) ) . toEqual ( [ ] ) ;
104- expect ( undeliveredStorageAttributes ( 'id' , { type : 'string ' , unique : true } ) ) . toEqual ( [ ] ) ; // the PK is unique
105- expect ( undeliveredStorageAttributes ( 'id' , { type : 'string ' , storage : { notNull : true } } ) ) . toEqual ( [ ] ) ; // the PK is NOT NULL
140+ expect ( undeliveredStorageAttributes ( 'id' , { type : 'text ' , maxLength : 255 } ) ) . toEqual ( [ ] ) ;
141+ expect ( undeliveredStorageAttributes ( 'id' , { type : 'text ' , unique : true } ) ) . toEqual ( [ ] ) ; // the PK is unique
142+ expect ( undeliveredStorageAttributes ( 'id' , { type : 'text ' , storage : { notNull : true } } ) ) . toEqual ( [ ] ) ; // the PK is NOT NULL
106143 expect ( undeliveredStorageAttributes ( 'created_at' , { type : 'datetime' , defaultValue : 'now()' } ) ) . toEqual ( [ ] ) ; // token, case-insensitive
107144 } ) ;
108145
109146 it ( 'FIRES for a storage attribute the column does NOT deliver, one entry each' , ( ) => {
110- expect ( keysOf ( undeliveredStorageAttributes ( 'id' , { type : 'string ' , maxLength : 12 } ) ) ) . toEqual ( [ 'maxLength' ] ) ;
111- expect ( keysOf ( undeliveredStorageAttributes ( 'id' , { type : 'string ' , defaultValue : 'NOW()' } ) ) ) . toEqual ( [ 'defaultValue' ] ) ;
147+ expect ( keysOf ( undeliveredStorageAttributes ( 'id' , { type : 'text ' , maxLength : 12 } ) ) ) . toEqual ( [ 'maxLength' ] ) ;
148+ expect ( keysOf ( undeliveredStorageAttributes ( 'id' , { type : 'text ' , defaultValue : 'NOW()' } ) ) ) . toEqual ( [ 'defaultValue' ] ) ;
112149 // created_at IS nullable and NOT unique — asking for either is a real disagreement.
113150 expect ( keysOf ( undeliveredStorageAttributes ( 'created_at' , { type : 'datetime' , unique : true } ) ) ) . toEqual ( [ 'unique' ] ) ;
114151 expect ( keysOf ( undeliveredStorageAttributes ( 'created_at' , { type : 'datetime' , storage : { notNull : true } } ) ) )
115152 . toEqual ( [ 'storage.notNull' ] ) ;
116153 // Several at once, in declaration order.
117- expect ( keysOf ( undeliveredStorageAttributes ( 'id' , { type : 'text ' , maxLength : 12 , unique : false } ) ) )
154+ expect ( keysOf ( undeliveredStorageAttributes ( 'id' , { type : 'number ' , maxLength : 12 , unique : false } ) ) )
118155 . toEqual ( [ 'type' , 'maxLength' ] ) ; // `unique: false` asks for nothing
119156 } ) ;
120157
@@ -125,7 +162,7 @@ describe('what a declaration on a builtin column name loses (#12015)', () => {
125162 it ( 'stays silent — never throws — on a key it does not know, and on a malformed declaration' , ( ) => {
126163 // Forward compatibility: an unclassified key cannot invent a warning. The
127164 // exhaustiveness case above is what makes its arrival visible.
128- expect ( undeliveredStorageAttributes ( 'id' , { type : 'string ' , someFutureKey : 'x' } as any ) ) . toEqual ( [ ] ) ;
165+ expect ( undeliveredStorageAttributes ( 'id' , { type : 'text ' , someFutureKey : 'x' } as any ) ) . toEqual ( [ ] ) ;
129166 expect ( undeliveredStorageAttributes ( 'id' , undefined ) ) . toEqual ( [ ] ) ;
130167 expect ( undeliveredStorageAttributes ( 'id' , null as any ) ) . toEqual ( [ ] ) ;
131168 } ) ;
0 commit comments