@@ -1380,6 +1380,75 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
13801380 } ) ;
13811381 } ) ;
13821382
1383+ // #3493 — an opt-in "historical" import (context.preserveAudit) reinstates the
1384+ // ORIGINAL timeline on UPDATE: the audit hook keeps a client-supplied
1385+ // updated_at/updated_by (instead of stamping now/importer), and the readonly
1386+ // strip admits the audit family + author-declared business readonly fields
1387+ // (closed_at) instead of dropping them. A normal write (no flag) still stamps
1388+ // and strips. (The driver's own updated_at force-stamp is covered separately in
1389+ // driver-sql's timestamp-format test — this mock driver echoes the data.)
1390+ describe ( 'preserveAudit — historical import keeps the original timeline on UPDATE (#3493)' , ( ) => {
1391+ async function bootHistorical ( ) {
1392+ const updates : Record < string , any > [ ] = [ ] ;
1393+ const mockDriver = {
1394+ name : 'hist-capture' , version : '1.0.0' ,
1395+ connect : async ( ) => { } , disconnect : async ( ) => { } ,
1396+ find : async ( ) => [ ] , findOne : async ( ) => null ,
1397+ create : async ( _o : string , d : any ) => ( { id : 'rec-1' , ...d } ) ,
1398+ update : async ( _o : string , _i : any , d : any ) => { updates . push ( { ...d } ) ; return { id : _i , ...d } ; } ,
1399+ delete : async ( ) => true , syncSchema : async ( ) => { } ,
1400+ } ;
1401+ await kernel . use ( {
1402+ name : 'hist-capture-plugin' , type : 'driver' , version : '1.0.0' ,
1403+ init : async ( ctx ) => { ctx . registerService ( 'driver.hist-capture' , mockDriver ) ; } ,
1404+ } ) ;
1405+ await kernel . use ( new ObjectQLPlugin ( ) ) ;
1406+ await kernel . bootstrap ( ) ;
1407+ const objectql = kernel . getService ( 'objectql' ) as any ;
1408+ const obj : ObjectSchema = {
1409+ name : 'ticket_obj' , label : 'Ticket' , datasource : 'hist-capture' ,
1410+ fields : {
1411+ name : { name : 'name' , label : 'Name' , type : 'text' } ,
1412+ updated_by : { name : 'updated_by' , label : 'Updated By' , type : 'text' , readonly : true } as any ,
1413+ // author-declared business readonly field — a case's close time
1414+ closed_at : { name : 'closed_at' , label : 'Closed At' , type : 'datetime' , readonly : true } as any ,
1415+ } ,
1416+ } ;
1417+ objectql . registry . registerObject ( obj , 'test' , 'test' ) ;
1418+ return { objectql, updates } ;
1419+ }
1420+
1421+ it ( 'KEEPS a supplied updated_at / updated_by / closed_at under preserveAudit' , async ( ) => {
1422+ const { objectql, updates } = await bootHistorical ( ) ;
1423+ const ts = '2021-03-01T09:00:00.000Z' ;
1424+ await objectql . update (
1425+ 'ticket_obj' ,
1426+ { id : 'rec-1' , name : 'B' , updated_at : ts , updated_by : 'u_original' , closed_at : ts } ,
1427+ { context : { userId : 'u_import' , preserveAudit : true } } ,
1428+ ) ;
1429+ expect ( updates . length ) . toBe ( 1 ) ;
1430+ const data = updates [ 0 ] ;
1431+ expect ( data . name ) . toBe ( 'B' ) ;
1432+ expect ( data . updated_at ) . toBe ( ts ) ; // hook kept client value (not "now")
1433+ expect ( data . updated_by ) . toBe ( 'u_original' ) ; // hook kept client value (not importer)
1434+ expect ( data . closed_at ) . toBe ( ts ) ; // business readonly reinstated, not stripped
1435+ } ) ;
1436+
1437+ it ( 'a normal update (no preserveAudit) still overwrites updated_by and strips closed_at' , async ( ) => {
1438+ const { objectql, updates } = await bootHistorical ( ) ;
1439+ await objectql . update (
1440+ 'ticket_obj' ,
1441+ { id : 'rec-1' , name : 'B' , closed_at : '2021-03-01T09:00:00.000Z' } ,
1442+ { context : { userId : 'u_import' } } ,
1443+ ) ;
1444+ expect ( updates . length ) . toBe ( 1 ) ;
1445+ const data = updates [ 0 ] ;
1446+ expect ( data . name ) . toBe ( 'B' ) ;
1447+ expect ( data . updated_by ) . toBe ( 'u_import' ) ; // server-stamped to the actor
1448+ expect ( data ) . not . toHaveProperty ( 'closed_at' ) ; // readonly business field stripped
1449+ } ) ;
1450+ } ) ;
1451+
13831452 // #3042 — conditional `readonlyWhen` must be enforced on the BULK
13841453 // (updateMany) path too, not only the single-id path. The bulk strip reads
13851454 // the matched rows' prior state and drops a field locked in ≥1 of them.
0 commit comments