1313
1414import { describe , it , expect , beforeAll , afterAll } from 'vitest' ;
1515import showcaseStack from '@objectstack/example-showcase' ;
16+ import { SECRET_MASK } from '@objectstack/objectql' ;
1617import { bootDogfoodStack , type DogfoodStack } from '../src/harness.js' ;
1718
1819// A field-type coverage entry. `write` is the value POSTed; `expect` describes
1920// how the value must come back. `equal` = exact (or set-equal for arrays);
20- // `computed`/`present` cover server-owned fields you don't write.
21+ // `computed`/`present` cover server-owned fields you don't write; `masked`
22+ // covers `secret`, which is encrypt-on-write and masked on read.
2123type Check =
2224 | { kind : 'equal' ; write : unknown }
2325 | { kind : 'setEqual' ; write : unknown [ ] }
24- | { kind : 'present' } // server-assigned, just must be non-null
26+ | { kind : 'present' ; write ?: unknown } // written but only asserted non-null (e.g. one-way/opaque)
27+ | { kind : 'masked' ; write : unknown } // secret: POSTed plaintext must read back as SECRET_MASK
2528 | { kind : 'computed' ; expected : unknown } ; // derived, asserted not written
2629
2730interface FieldCase {
@@ -64,13 +67,45 @@ const MATRIX: FieldCase[] = [
6467 { field : 'f_tags' , type : 'tags' , check : { kind : 'setEqual' , write : [ 'alpha' , 'beta' , 'gamma' ] } } ,
6568 // numeric scalar — same fidelity class as rating/slider (was TEXT-affinity)
6669 { field : 'f_progress' , type : 'progress' , check : { kind : 'equal' , write : 60 } } ,
70+ // rich text — all plain strings on the wire
71+ { field : 'f_markdown' , type : 'markdown' , check : { kind : 'equal' , write : '# Heading\n\nbody' } } ,
72+ { field : 'f_html' , type : 'html' , check : { kind : 'equal' , write : '<p>hi</p>' } } ,
73+ { field : 'f_richtext' , type : 'richtext' , check : { kind : 'equal' , write : '<b>rich</b>' } } ,
74+ { field : 'f_code' , type : 'code' , check : { kind : 'equal' , write : '{\n "a": 1\n}' } } ,
75+ { field : 'f_signature' , type : 'signature' , check : { kind : 'equal' , write : 'data:image/png;base64,AAAA' } } ,
76+ { field : 'f_qrcode' , type : 'qrcode' , check : { kind : 'equal' , write : 'https://objectstack.ai' } } ,
77+ // temporal — instant
78+ { field : 'f_datetime' , type : 'datetime' , check : { kind : 'equal' , write : '2024-03-15T14:30:00.000Z' } } ,
6779 // structured JSON
6880 { field : 'f_json' , type : 'json' , check : { kind : 'equal' , write : { a : 1 , b : [ 2 , 3 ] } } } ,
6981 { field : 'f_color' , type : 'color' , check : { kind : 'equal' , write : '#FF8800' } } ,
82+ { field : 'f_vector' , type : 'vector' , check : { kind : 'equal' , write : [ 0.1 , 0.2 , 0.3 ] } } ,
7083 // object-valued types that must store/parse as JSON, not stringify to TEXT
7184 { field : 'f_record' , type : 'record' , check : { kind : 'equal' , write : { home : '+1' , work : '+2' } } } ,
7285 { field : 'f_video' , type : 'video' , check : { kind : 'equal' , write : { url : 'https://cdn/v.mp4' , duration : 12 } } } ,
7386 { field : 'f_audio' , type : 'audio' , check : { kind : 'equal' , write : { url : 'https://cdn/a.mp3' , duration : 30 } } } ,
87+ { field : 'f_composite' , type : 'composite' , check : { kind : 'equal' , write : { label : 'x' , n : 1 } } } ,
88+ { field : 'f_repeater' , type : 'repeater' , check : { kind : 'equal' , write : [ { a : 1 } , { a : 2 } ] } } ,
89+ { field : 'f_location' , type : 'location' , check : { kind : 'equal' , write : { lat : 37.77 , lng : - 122.42 } } } ,
90+ { field : 'f_address' , type : 'address' , check : { kind : 'equal' , write : { street : '1 Main' , city : 'SF' , country : 'US' } } } ,
91+ { field : 'f_image' , type : 'image' , check : { kind : 'equal' , write : { url : 'https://cdn/i.png' , alt : 'i' } } } ,
92+ { field : 'f_file' , type : 'file' , check : { kind : 'equal' , write : { url : 'https://cdn/f.pdf' , name : 'f.pdf' , size : 1024 } } } ,
93+ { field : 'f_avatar' , type : 'avatar' , check : { kind : 'equal' , write : { url : 'https://cdn/a.png' } } } ,
94+ // relational — store a reference id as a string and read it back verbatim.
95+ // FK enforcement is off in this harness, so this asserts value fidelity
96+ // (id string → id string), not referential integrity / $expand (covered
97+ // elsewhere). The point here is the stored type doesn't drift.
98+ { field : 'f_lookup' , type : 'lookup' , check : { kind : 'equal' , write : 'acc_synthetic_0001' } } ,
99+ { field : 'f_master_detail' , type : 'master_detail' , check : { kind : 'equal' , write : 'proj_synthetic_0001' } } ,
100+ { field : 'f_tree' , type : 'tree' , check : { kind : 'equal' , write : 'cat_synthetic_0001' } } ,
101+ // security — `secret` encrypts on write and masks on read; `password` is a
102+ // credential type the generic CRUD path stores opaquely (auth owns hashing),
103+ // so assert it merely persists rather than asserting a plaintext contract.
104+ { field : 'f_secret' , type : 'secret' , check : { kind : 'masked' , write : 'topsecret-value' } } ,
105+ { field : 'f_password' , type : 'password' , check : { kind : 'present' , write : 'p@ssw0rd!' } } ,
106+ // NB: f_summary (roll-up) is intentionally absent — it's a computed
107+ // aggregate over related records, null on a childless fixture row; its
108+ // semantics are covered by dedicated roll-up tests, not value fidelity.
74109 // computed / system — not written, must materialize
75110 { field : 'f_autonumber' , type : 'autonumber' , check : { kind : 'present' } } ,
76111 // f_number(42) * f_percent(75) / 100 = 31.5
@@ -85,11 +120,11 @@ describe('dogfood: field-type capability matrix round-trips over HTTP (#2004)',
85120 stack = await bootDogfoodStack ( showcaseStack ) ;
86121 const token = await stack . signIn ( ) ;
87122
88- // Build the create body from every `equal`/`setEqual` entry (+ required name).
123+ // Build the create body from every entry that carries a `write` value
124+ // (+ required name). `present`/`computed` server-owned fields are skipped.
89125 const body : Record < string , unknown > = { name : 'zoo-roundtrip' } ;
90126 for ( const c of MATRIX ) {
91- if ( c . check . kind === 'equal' ) body [ c . field ] = c . check . write ;
92- else if ( c . check . kind === 'setEqual' ) body [ c . field ] = c . check . write ;
127+ if ( 'write' in c . check && c . check . write !== undefined ) body [ c . field ] = c . check . write ;
93128 }
94129
95130 const created = await stack . apiAs ( token , 'POST' , '/data/showcase_field_zoo' , body ) ;
@@ -127,7 +162,13 @@ describe('dogfood: field-type capability matrix round-trips over HTTP (#2004)',
127162 break ;
128163 }
129164 case 'present' :
130- expect ( actual ?? null , `${ c . field } should be server-assigned` ) . not . toBeNull ( ) ;
165+ expect ( actual ?? null , `${ c . field } should be persisted` ) . not . toBeNull ( ) ;
166+ break ;
167+ case 'masked' :
168+ // secret never leaves the engine as plaintext — the write value is
169+ // encrypted into sys_secret and the read path returns the mask.
170+ expect ( actual , `${ c . field } should not echo plaintext` ) . not . toEqual ( c . check . write ) ;
171+ expect ( actual ) . toEqual ( SECRET_MASK ) ;
131172 break ;
132173 case 'computed' :
133174 expect ( Number ( actual ) ) . toBeCloseTo ( Number ( c . check . expected ) , 5 ) ;
0 commit comments