@@ -42,21 +42,18 @@ import { ObjectQL } from './engine.js';
4242const REMOTE = 'remote_ds' ;
4343const PACKAGE_ID = 'test-21910' ;
4444
45- type Rows = Map < string , Map < string , Record < string , unknown > > > ;
45+ type Row = Record < string , unknown > ;
4646
4747/**
4848 * A stub driver that records every read into a shared log and can be told to
49- * refuse reads of one object with an exact error object.
49+ * refuse reads of one object with an exact error object. Its `find` applies
50+ * the caller's `limit` after the filter, by presence.
5051 */
5152function makeDriver ( name : string , log : { reads : string [ ] ; begun : number } ) {
52- const stores : Rows = new Map ( ) ;
53+ const tables : Record < string , Row [ ] > = { } ;
5354 const failReads = new Map < string , unknown > ( ) ;
54- const storeFor = ( o : string ) => {
55- let s = stores . get ( o ) ;
56- if ( ! s ) { s = new Map ( ) ; stores . set ( o , s ) ; }
57- return s ;
58- } ;
59- const matches = ( row : Record < string , unknown > , where : any ) : boolean => {
55+ const rowsOf = ( o : string ) : Row [ ] => ( tables [ o ] ??= [ ] ) ;
56+ const matches = ( row : Row , where : any ) : boolean => {
6057 if ( ! where || typeof where !== 'object' ) return true ;
6158 for ( const [ k , v ] of Object . entries ( where ) ) {
6259 if ( k . startsWith ( '$' ) ) continue ;
@@ -65,47 +62,55 @@ function makeDriver(name: string, log: { reads: string[]; begun: number }) {
6562 }
6663 return true ;
6764 } ;
68- const gate = ( o : string ) => {
69- log . reads . push ( o ) ;
70- if ( failReads . has ( o ) ) throw failReads . get ( o ) ;
71- } ;
7265 const driver : any = {
7366 name, version : '0.0.0' , supports : { } ,
7467 async connect ( ) { } , async disconnect ( ) { } , async checkHealth ( ) { return true ; } , async execute ( ) { return null ; } ,
7568 async syncSchema ( ) { } ,
7669 registerExternalObject ( ) { } ,
7770 async find ( o : string , ast : any ) {
78- gate ( o ) ;
79- return Array . from ( storeFor ( o ) . values ( ) ) . filter ( ( r ) => matches ( r , ast ?. where ) ) ;
71+ log . reads . push ( o ) ;
72+ const failure = failReads . get ( o ) ;
73+ if ( failure !== undefined ) throw failure ;
74+ const hit = ( tables [ o ] ?? [ ] ) . filter ( ( r ) => matches ( r , ast ?. where ) ) ;
75+ return typeof ast ?. limit === 'number' ? hit . slice ( 0 , ast . limit ) : hit ;
8076 } ,
8177 async findOne ( o : string , ast : any ) {
82- gate ( o ) ;
83- for ( const r of storeFor ( o ) . values ( ) ) if ( matches ( r , ast ?. where ) ) return r ;
84- return null ;
78+ const [ first ] = await this . find ( o , { ...ast , limit : 1 } ) ;
79+ return first ?? null ;
8580 } ,
8681 async count ( o : string , ast : any ) {
87- gate ( o ) ;
88- return Array . from ( storeFor ( o ) . values ( ) ) . filter ( ( r ) => matches ( r , ast ?. where ) ) . length ;
82+ return ( await this . find ( o , { where : ast ?. where } ) ) . length ;
8983 } ,
90- async create ( o : string , data : Record < string , unknown > ) {
84+ async create ( o : string , data : Row ) {
9185 const row = { ...data , id : String ( data . id ) } ;
92- storeFor ( o ) . set ( row . id , row ) ;
86+ rowsOf ( o ) . push ( row ) ;
9387 return row ;
9488 } ,
95- async update ( o : string , id : string , data : Record < string , unknown > ) {
96- const cur = storeFor ( o ) . get ( String ( id ) ) ;
97- if ( ! cur ) throw new Error ( `not found ${ o } /${ id } ` ) ;
98- const up = { ...cur , ...data , id : String ( id ) } ;
99- storeFor ( o ) . set ( String ( id ) , up ) ;
100- return up ;
89+ async update ( o : string , id : string , data : Row ) {
90+ const rows = rowsOf ( o ) ;
91+ const at = rows . findIndex ( ( r ) => r . id === String ( id ) ) ;
92+ if ( at < 0 ) throw new Error ( `not found ${ o } /${ id } ` ) ;
93+ rows [ at ] = { ...rows [ at ] , ...data , id : String ( id ) } ;
94+ return rows [ at ] ;
95+ } ,
96+ async upsert ( o : string , data : Row ) { return this . create ( o , data ) ; } ,
97+ async delete ( o : string , id : string ) {
98+ const rows = rowsOf ( o ) ;
99+ const at = rows . findIndex ( ( r ) => r . id === String ( id ) ) ;
100+ if ( at < 0 ) return false ;
101+ rows . splice ( at , 1 ) ;
102+ return true ;
101103 } ,
102- async upsert ( o : string , data : Record < string , unknown > ) { return this . create ( o , data ) ; } ,
103- async delete ( o : string , id : string ) { return storeFor ( o ) . delete ( String ( id ) ) ; } ,
104104 async bulkCreate ( ) { return [ ] ; } , async bulkUpdate ( ) { return [ ] ; } , async bulkDelete ( ) { } ,
105105 async beginTransaction ( ) { log . begun += 1 ; return { id : `trx_${ log . begun } ` } ; } ,
106106 async commit ( ) { } , async rollback ( ) { } ,
107107 } ;
108- return { driver, stores, failReads, seed : ( o : string , row : Record < string , unknown > ) => storeFor ( o ) . set ( String ( row . id ) , row ) } ;
108+ return {
109+ driver,
110+ failReads,
111+ has : ( o : string , id : string ) => rowsOf ( o ) . some ( ( r ) => r . id === id ) ,
112+ seed : ( o : string , row : Row ) => void rowsOf ( o ) . push ( row ) ,
113+ } ;
109114}
110115
111116/** The deleted object. Its own injected anchor makes it self-referencing, harmlessly. */
@@ -202,7 +207,7 @@ describe('[#21910] the cascade scan skips a federated object\'s injected tenant
202207 log . reads . length = 0 ;
203208 await engine . delete ( 'sys_organization' , { where : { id : ORG_ID } } as any ) ;
204209
205- expect ( local . stores . get ( 'sys_organization' ) ?. has ( ORG_ID ) ) . toBe ( false ) ;
210+ expect ( local . has ( 'sys_organization' , ORG_ID ) ) . toBe ( false ) ;
206211 expect ( log . reads ) . not . toContain ( 'ext_customer' ) ;
207212 // CONTROL: the scan ran for this delete, and probed the local object's injected anchor.
208213 expect ( log . reads ) . toContain ( 'acct' ) ;
@@ -222,7 +227,7 @@ describe('[#21910] the cascade scan skips a federated object\'s injected tenant
222227 expect ( err . code ) . toBe ( 'INVALID_FILTER' ) ;
223228 expect ( err . status ) . toBe ( 400 ) ;
224229 expect ( log . reads ) . toContain ( 'ext_tenant_customer' ) ;
225- expect ( local . stores . get ( 'sys_organization' ) ?. has ( ORG_ID ) ) . toBe ( true ) ;
230+ expect ( local . has ( 'sys_organization' , ORG_ID ) ) . toBe ( true ) ;
226231 } ) ;
227232
228233 it ( 'still probes any other lookup the author declared on a federated object, and its failure propagates (#8895)' , async ( ) => {
@@ -237,7 +242,7 @@ describe('[#21910] the cascade scan skips a federated object\'s injected tenant
237242 expect ( err . code ) . toBe ( 'INVALID_FILTER' ) ;
238243 expect ( err . status ) . toBe ( 400 ) ;
239244 expect ( log . reads ) . toContain ( 'ext_order' ) ;
240- expect ( local . stores . get ( 'sys_organization' ) ?. has ( ORG_ID ) ) . toBe ( true ) ;
245+ expect ( local . has ( 'sys_organization' , ORG_ID ) ) . toBe ( true ) ;
241246 } ) ;
242247} ) ;
243248
@@ -247,7 +252,7 @@ describe('[#21910] the cascade atomicity plan agrees with the scan about who tak
247252
248253 await engine . delete ( 'sys_organization' , { where : { id : ORG_ID } } as any ) ;
249254
250- expect ( local . stores . get ( 'sys_organization' ) ?. has ( ORG_ID ) ) . toBe ( false ) ;
255+ expect ( local . has ( 'sys_organization' , ORG_ID ) ) . toBe ( false ) ;
251256 expect ( log . begun ) . toBe ( 1 ) ;
252257 expect ( warnings . filter ( ( w ) => w . includes ( NOT_ATOMIC ) ) ) . toEqual ( [ ] ) ;
253258 } ) ;
@@ -257,7 +262,7 @@ describe('[#21910] the cascade atomicity plan agrees with the scan about who tak
257262
258263 await engine . delete ( 'sys_organization' , { where : { id : ORG_ID } } as any ) ;
259264
260- expect ( local . stores . get ( 'sys_organization' ) ?. has ( ORG_ID ) ) . toBe ( false ) ;
265+ expect ( local . has ( 'sys_organization' , ORG_ID ) ) . toBe ( false ) ;
261266 expect ( log . reads ) . toContain ( 'ext_order' ) ;
262267 expect ( log . begun ) . toBe ( 0 ) ;
263268 expect ( warnings . filter ( ( w ) => w . includes ( NOT_ATOMIC ) ) ) . toHaveLength ( 1 ) ;
0 commit comments