11import { randomUUID } from 'node:crypto'
2- import { describeAcceptance , encodePathSegments , getAcceptanceConfig } from '../support/config'
3- import { createRestClient } from '../support/http'
4- import {
5- cleanupRestResources ,
6- createRestBucket ,
7- requireServiceKey ,
8- uniqueBucketName ,
9- uniqueObjectKey ,
10- uploadRestObject ,
11- } from '../support/resources'
2+ import { setupLoopbackMessaging } from '@platformatic/runtime'
3+ import dotenv from 'dotenv'
4+ import { describeAcceptance , getAcceptanceConfig } from '../support/config'
5+ import { uniqueBucketName } from '../support/resources'
6+
7+ dotenv . config ( { path : '.env.test' , override : false } )
8+ dotenv . config ( { path : '.env' , override : false } )
9+
10+ type ApplicationContext = {
11+ close : ( ) => Promise < void >
12+ isBackgroundApplication : boolean
13+ }
1214
1315type DatabaseWattStats = {
1416 acquire : number
@@ -40,6 +42,10 @@ type QueryRowsResponse = {
4042 rows : Array < { value : number } >
4143}
4244
45+ type BucketRowsResponse = {
46+ rows : BucketResponse [ ]
47+ }
48+
4349type LockResponse = {
4450 lockId : string
4551}
@@ -48,16 +54,29 @@ type ErrorResponse = {
4854 code : string
4955 destination ?: string
5056 message : string
57+ sqlState ?: string
5158}
5259
5360const describeDatabaseWattAcceptance = process . env . ACCEPTANCE_DATABASE_WATT === 'true' ? describeAcceptance : describe . skip
61+ let app : ApplicationContext | undefined
62+ let messaging : ReturnType < typeof setupLoopbackMessaging > | undefined
63+
64+ async function createDatabaseWattApp ( ) : Promise < ApplicationContext > {
65+ const moduleUrl = new URL ( '../../src/database/index.ts' , import . meta. url ) . href
66+ const databaseApp = await import ( moduleUrl )
67+ return databaseApp . create ( )
68+ }
5469
5570function testDestination ( ) : string {
5671 return process . env . ACCEPTANCE_TENANT_ID || process . env . TENANT_ID || 'default'
5772}
5873
5974function sendDatabaseMessage < T = unknown > ( message : string , payload : unknown ) : Promise < T > {
60- return sendWattMessage < T > ( 'database' , message , payload )
75+ if ( ! messaging ) {
76+ throw new Error ( 'Database Watt loopback messaging is not initialized' )
77+ }
78+
79+ return messaging . send ( 'database' , message , payload ) as Promise < T >
6180}
6281
6382function isDatabaseError ( result : unknown ) : result is ErrorResponse {
@@ -89,6 +108,75 @@ async function queryDatabaseWatt(destination = testDestination()): Promise<Query
89108 } )
90109}
91110
111+ async function cleanupBucket ( bucketName : string , destination = testDestination ( ) ) : Promise < void > {
112+ const tx = await beginTransaction ( destination )
113+
114+ try {
115+ await checkedResult (
116+ sendDatabaseMessage ( 'database.lockedQuery' , {
117+ lockId : tx . lockId ,
118+ requestId : randomUUID ( ) ,
119+ sql : `SELECT set_config('storage.allow_delete_query', 'true', true)` ,
120+ } )
121+ )
122+ await checkedResult (
123+ sendDatabaseMessage ( 'database.lockedQuery' , {
124+ lockId : tx . lockId ,
125+ requestId : randomUUID ( ) ,
126+ sql : 'DELETE FROM storage.buckets WHERE id = $1' ,
127+ values : [ bucketName ] ,
128+ } )
129+ )
130+ await checkedResult ( sendDatabaseMessage ( 'database.commitTransaction' , { lockId : tx . lockId } ) )
131+ } catch ( error ) {
132+ await sendDatabaseMessage ( 'database.rollbackTransaction' , { lockId : tx . lockId } ) . catch ( ( ) => undefined )
133+ throw error
134+ }
135+ }
136+
137+ async function getBucket ( bucketName : string , destination = testDestination ( ) ) : Promise < BucketResponse | undefined > {
138+ const result = await checkedResult < BucketRowsResponse > (
139+ sendDatabaseMessage ( 'database.query' , {
140+ destination,
141+ requestId : randomUUID ( ) ,
142+ sql : 'SELECT id, name, public FROM storage.buckets WHERE id = $1' ,
143+ values : [ bucketName ] ,
144+ } )
145+ )
146+
147+ return result . rows [ 0 ]
148+ }
149+
150+ async function insertBucket ( bucketName : string , destination = testDestination ( ) ) : Promise < unknown > {
151+ return sendDatabaseMessage ( 'database.query' , {
152+ destination,
153+ requestId : randomUUID ( ) ,
154+ sql : `INSERT INTO storage.buckets (id, name, owner, public) VALUES ($1, $1, $2, false)` ,
155+ values : [ bucketName , randomUUID ( ) ] ,
156+ } )
157+ }
158+
159+ async function commitBucketDatabaseWatt ( ) : Promise < { bucketName : string } > {
160+ const bucketName = uniqueBucketName ( 'dbwatt-commit' )
161+ const tx = await beginTransaction ( )
162+
163+ try {
164+ await checkedResult (
165+ sendDatabaseMessage ( 'database.lockedQuery' , {
166+ lockId : tx . lockId ,
167+ requestId : randomUUID ( ) ,
168+ sql : `INSERT INTO storage.buckets (id, name, owner, public) VALUES ($1, $1, $2, false)` ,
169+ values : [ bucketName , randomUUID ( ) ] ,
170+ } )
171+ )
172+ await checkedResult ( sendDatabaseMessage ( 'database.commitTransaction' , { lockId : tx . lockId } ) )
173+ return { bucketName }
174+ } catch ( error ) {
175+ await sendDatabaseMessage ( 'database.rollbackTransaction' , { lockId : tx . lockId } ) . catch ( ( ) => undefined )
176+ throw error
177+ }
178+ }
179+
92180async function masterTransaction ( ) : Promise < { value : number | undefined } > {
93181 const tx = await beginTransaction ( 'master' )
94182
@@ -224,15 +312,28 @@ async function resetDatabaseWattStats(): Promise<void> {
224312}
225313
226314describeDatabaseWattAcceptance (
227- 'Database Watt E2E ' ,
315+ 'Database Watt PostgreSQL integration ' ,
228316 {
229317 destructive : true ,
230318 profiles : [ 'core' ] ,
231319 } ,
232320 ( ) => {
233- it ( 'executes stateless queries in the Database Watt worker' , async ( ) => {
321+ beforeAll ( async ( ) => {
322+ messaging = setupLoopbackMessaging ( 'database-watt-acceptance' )
323+ app = await createDatabaseWattApp ( )
324+ } )
325+
326+ beforeEach ( async ( ) => {
234327 await resetDatabaseWattStats ( )
328+ } )
329+
330+ afterAll ( async ( ) => {
331+ await app ?. close ( )
332+ app = undefined
333+ messaging = undefined
334+ } )
235335
336+ it ( 'executes stateless queries in the Database Watt worker' , async ( ) => {
236337 const response = await queryDatabaseWatt ( )
237338 const stats = await getDatabaseWattStats ( )
238339
@@ -248,7 +349,6 @@ describeDatabaseWattAcceptance(
248349 return
249350 }
250351
251- await resetDatabaseWattStats ( )
252352 const response = await queryDatabaseWatt ( 'master' )
253353 const stats = await getDatabaseWattStats ( )
254354
@@ -264,7 +364,6 @@ describeDatabaseWattAcceptance(
264364 return
265365 }
266366
267- await resetDatabaseWattStats ( )
268367 const response = await masterTransaction ( )
269368 const stats = await getDatabaseWattStats ( )
270369
@@ -274,56 +373,38 @@ describeDatabaseWattAcceptance(
274373 expect ( stats . commitTransaction ) . toBeGreaterThanOrEqual ( 1 )
275374 } )
276375
277- it ( 'commits REST object changes through Database Watt transactions' , async ( ) => {
278- const client = createRestClient ( )
279- const token = requireServiceKey ( )
280- const bucketName = uniqueBucketName ( 'dbwatt-commit' )
281- const objectKey = uniqueObjectKey ( 'dbwatt-commit' )
282- await resetDatabaseWattStats ( )
283-
376+ it ( 'commits bucket changes through Database Watt transactions' , async ( ) => {
377+ let bucketName : string | undefined
284378 try {
285- await createRestBucket ( bucketName , { isPublic : false } )
286- await uploadRestObject ( bucketName , objectKey , 'database-watt-commit' )
287-
288- const read = await client . request ( 'GET' , `/object/${ bucketName } /${ encodePathSegments ( objectKey ) } ` , {
289- expectedStatus : 200 ,
290- token,
291- } )
379+ const committed = await commitBucketDatabaseWatt ( )
380+ bucketName = committed . bucketName
381+ const bucket = await getBucket ( bucketName )
292382 const stats = await getDatabaseWattStats ( )
293383
294- expect ( read . body ) . toBe ( 'database-watt-commit' )
384+ expect ( bucket ) . toMatchObject ( { id : bucketName , name : bucketName , public : false } )
295385 expect ( stats . beginTransaction ) . toBeGreaterThanOrEqual ( 1 )
296386 expect ( stats . lockedQuery ) . toBeGreaterThanOrEqual ( 1 )
297387 expect ( stats . commitTransaction ) . toBeGreaterThanOrEqual ( 1 )
298388 } finally {
299- await cleanupRestResources ( bucketName , [ objectKey ] , client )
389+ if ( bucketName ) {
390+ await cleanupBucket ( bucketName )
391+ }
300392 }
301393 } )
302394
303395 it ( 'rolls back failed transaction work and leaves no partial bucket state' , async ( ) => {
304- const client = createRestClient ( )
305- const token = requireServiceKey ( )
306- await resetDatabaseWattStats ( )
307-
308396 const rollback = await rollbackDatabaseWatt ( )
309397 const bucketName = rollback . bucketName
310398 expect ( bucketName ) . toBeTruthy ( )
311399
312- const lookup = await client . request ( 'GET' , `/bucket/${ bucketName } ` , {
313- expectedStatus : 400 ,
314- token,
315- } )
400+ const bucket = await getBucket ( bucketName )
316401 const stats = await getDatabaseWattStats ( )
317402
318- expect ( lookup . status ) . toBe ( 400 )
403+ expect ( bucket ) . toBeUndefined ( )
319404 expect ( stats . rollbackTransaction ) . toBeGreaterThanOrEqual ( 1 )
320405 } )
321406
322407 it ( 'preserves nested savepoint semantics in Database Watt transactions' , async ( ) => {
323- const client = createRestClient ( )
324- const token = requireServiceKey ( )
325- await resetDatabaseWattStats ( )
326-
327408 const savepoint = await savepointDatabaseWatt ( )
328409 const outerBucket = savepoint . outerBucket
329410 const innerBucket = savepoint . innerBucket
@@ -332,56 +413,37 @@ describeDatabaseWattAcceptance(
332413 expect ( outerBucket ) . toBeTruthy ( )
333414 expect ( innerBucket ) . toBeTruthy ( )
334415
335- const outer = await client . request < BucketResponse > ( 'GET' , `/bucket/${ outerBucket } ` , {
336- expectedStatus : 200 ,
337- token,
338- } )
339- const inner = await client . request ( 'GET' , `/bucket/${ innerBucket } ` , {
340- expectedStatus : 400 ,
341- token,
342- } )
416+ const outer = await getBucket ( outerBucket )
417+ const inner = await getBucket ( innerBucket )
343418 const stats = await getDatabaseWattStats ( )
344419
345- expect ( outer . json ?. id ) . toBe ( outerBucket )
346- expect ( inner . status ) . toBe ( 400 )
420+ expect ( outer ?. id ) . toBe ( outerBucket )
421+ expect ( inner ) . toBeUndefined ( )
347422 expect ( stats . lockedQuery ) . toBeGreaterThanOrEqual ( 3 )
348423 expect ( stats . commitTransaction ) . toBeGreaterThanOrEqual ( 1 )
349424 } finally {
350425 if ( outerBucket ) {
351- await cleanupRestResources ( outerBucket , [ ] , client )
426+ await cleanupBucket ( outerBucket )
352427 }
353428 }
354429 } )
355430
356- it ( 'preserves storage error mapping when Database Watt returns PostgreSQL errors' , async ( ) => {
357- const client = createRestClient ( )
358- const token = requireServiceKey ( )
431+ it ( 'preserves PostgreSQL error mapping when Database Watt returns PostgreSQL errors' , async ( ) => {
359432 const bucketName = uniqueBucketName ( 'dbwatt-error' )
360- await resetDatabaseWattStats ( )
361433
362434 try {
363- await createRestBucket ( bucketName , { isPublic : false } )
364- const duplicate = await client . request ( 'POST' , '/bucket' , {
365- body : {
366- id : bucketName ,
367- name : bucketName ,
368- public : false ,
369- } ,
370- expectedStatus : 400 ,
371- token,
372- } )
435+ await checkedResult ( insertBucket ( bucketName ) )
436+ const duplicate = await insertBucket ( bucketName ) as ErrorResponse
373437 const stats = await getDatabaseWattStats ( )
374438
375- expect ( duplicate . status ) . toBe ( 400 )
376- expect ( stats . lockedQuery ) . toBeGreaterThanOrEqual ( 1 )
439+ expect ( duplicate ) . toMatchObject ( { code : 'POSTGRES_ERROR' , sqlState : '23505' } )
440+ expect ( stats . query ) . toBeGreaterThanOrEqual ( 2 )
377441 } finally {
378- await cleanupRestResources ( bucketName , [ ] , client )
442+ await cleanupBucket ( bucketName )
379443 }
380444 } )
381445
382446 it ( 'translates request aborts into Database Watt cancellation' , async ( ) => {
383- await resetDatabaseWattStats ( )
384-
385447 const response = await sleepDatabaseWatt ( )
386448 const stats = await getDatabaseWattStats ( )
387449
@@ -397,16 +459,13 @@ describeDatabaseWattAcceptance(
397459 return
398460 }
399461
400- await resetDatabaseWattStats ( )
401462 const response = await missingDestinationDatabaseWatt ( )
402463
403464 expect ( response ) . toMatchObject ( { code : 'DESTINATION_UNKNOWN' } )
404465 expect ( response . destination ) . toEqual ( expect . stringMatching ( / ^ m i s s i n g - / ) )
405466 } )
406467
407468 it ( 'handles concurrent Database Watt query load' , async ( ) => {
408- await resetDatabaseWattStats ( )
409-
410469 const response = await concurrentQueriesDatabaseWatt ( )
411470 const stats = await getDatabaseWattStats ( )
412471
@@ -416,28 +475,22 @@ describeDatabaseWattAcceptance(
416475
417476 it ( 'exercises multitenant destination resolution when the target is multitenant' , async ( ) => {
418477 const config = getAcceptanceConfig ( )
419- const client = createRestClient ( )
420- const token = requireServiceKey ( config )
421478 const bucketName = uniqueBucketName ( 'dbwatt-tenant' )
422479
423480 if ( ! config . tenantId ) {
424481 expect ( config . tenantId ) . toBeUndefined ( )
425482 return
426483 }
427484
428- await resetDatabaseWattStats ( )
429485 try {
430- await createRestBucket ( bucketName , { isPublic : false } )
431- const bucket = await client . request < BucketResponse > ( 'GET' , `/bucket/${ bucketName } ` , {
432- expectedStatus : 200 ,
433- token,
434- } )
486+ await checkedResult ( insertBucket ( bucketName , config . tenantId ) )
487+ const bucket = await getBucket ( bucketName , config . tenantId )
435488 const stats = await getDatabaseWattStats ( )
436489
437- expect ( bucket . json ?. id ) . toBe ( bucketName )
438- expect ( stats . beginTransaction ) . toBeGreaterThanOrEqual ( 1 )
490+ expect ( bucket ?. id ) . toBe ( bucketName )
491+ expect ( stats . query ) . toBeGreaterThanOrEqual ( 2 )
439492 } finally {
440- await cleanupRestResources ( bucketName , [ ] , client )
493+ await cleanupBucket ( bucketName , config . tenantId )
441494 }
442495 } )
443496 }
0 commit comments