Skip to content

Commit 9b3c461

Browse files
refactor(storage-confirm-upload): use node-sql template and storage_module metadata
- Switch from node-graphql to node-sql template (connection pool) - Query storage_module metadata for schema/table names instead of string manipulation - Use QuoteUtils.quoteQualifiedIdentifier for SQL identifier quoting - Standardize env vars (CDN_ENDPOINT, AWS_ACCESS_KEY, AWS_REGION) - Update tests for pool-based context Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent dbdb3d4 commit 9b3c461

4 files changed

Lines changed: 252 additions & 170 deletions

File tree

functions/storage-confirm-upload/__tests__/handler.test.ts

Lines changed: 128 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,31 @@ jest.mock('@aws-sdk/client-s3', () => ({
88
HeadObjectCommand: jest.fn().mockImplementation((params) => params)
99
}));
1010

11-
const mockPgQuery = jest.fn();
12-
const mockPgConnect = jest.fn();
13-
const mockPgEnd = jest.fn();
14-
jest.mock('pg', () => ({
15-
Client: jest.fn().mockImplementation(() => ({
16-
connect: mockPgConnect,
17-
query: mockPgQuery,
18-
end: mockPgEnd
19-
}))
20-
}));
11+
const mockStorageModuleRow = {
12+
id: 'sm-123',
13+
buckets_schema: 'test-db-storage-public',
14+
buckets_table: 'app_buckets',
15+
files_schema: 'test-db-storage-public',
16+
files_table: 'app_files',
17+
endpoint: null,
18+
provider: 'minio',
19+
};
20+
21+
const createMockPool = () => {
22+
const mockQuery = jest.fn();
23+
const mockRelease = jest.fn();
24+
const mockClient = {
25+
query: mockQuery,
26+
release: mockRelease
27+
};
28+
const pool = {
29+
connect: jest.fn().mockResolvedValue(mockClient),
30+
query: mockQuery,
31+
_mockClient: mockClient,
32+
_mockQuery: mockQuery
33+
};
34+
return pool;
35+
};
2136

2237
const loadHandler = () => {
2338
const mod = require('../handler');
@@ -44,58 +59,96 @@ describe('storage-confirm-upload handler', () => {
4459
describe('validation', () => {
4560
it('throws on missing file_id', async () => {
4661
const handler = loadHandler();
62+
const pool = createMockPool();
4763
await expect(
4864
handler(
4965
{ key: 'test.txt', bucket_id: 'bucket-123' },
50-
createMockContext()
66+
createMockContext({ pool, databaseId: 'db-123' })
5167
)
5268
).rejects.toThrow('Missing required fields');
5369
});
5470

5571
it('throws on missing key', async () => {
5672
const handler = loadHandler();
73+
const pool = createMockPool();
5774
await expect(
5875
handler(
5976
{ file_id: '123', bucket_id: 'bucket-123' },
60-
createMockContext()
77+
createMockContext({ pool, databaseId: 'db-123' })
6178
)
6279
).rejects.toThrow('Missing required fields');
6380
});
6481

6582
it('throws on missing bucket_id', async () => {
6683
const handler = loadHandler();
84+
const pool = createMockPool();
6785
await expect(
6886
handler(
6987
{ file_id: '123', key: 'test.txt' },
70-
createMockContext()
88+
createMockContext({ pool, databaseId: 'db-123' })
7189
)
7290
).rejects.toThrow('Missing required fields');
7391
});
92+
93+
it('throws on missing databaseId', async () => {
94+
const handler = loadHandler();
95+
const pool = createMockPool();
96+
await expect(
97+
handler(
98+
{ file_id: '123', key: 'test.txt', bucket_id: 'bucket-123' },
99+
createMockContext({ pool, databaseId: undefined })
100+
)
101+
).rejects.toThrow('Missing databaseId');
102+
});
103+
});
104+
105+
describe('storage module lookup', () => {
106+
it('throws when storage module not found', async () => {
107+
const handler = loadHandler();
108+
const pool = createMockPool();
109+
pool._mockQuery.mockResolvedValueOnce({ rows: [] }); // No storage module
110+
111+
await expect(
112+
handler(
113+
{ file_id: '123', key: 'test.txt', bucket_id: 'bucket-123' },
114+
createMockContext({ pool, databaseId: 'db-123' })
115+
)
116+
).rejects.toThrow('STORAGE_MODULE_NOT_FOUND');
117+
});
74118
});
75119

76120
describe('S3 file check', () => {
77121
it('throws when file not found in S3', async () => {
78122
const handler = loadHandler();
123+
const pool = createMockPool();
124+
// Storage module query
125+
pool._mockQuery.mockResolvedValueOnce({ rows: [mockStorageModuleRow] });
126+
// Bucket type query
127+
pool._mockQuery.mockResolvedValueOnce({ rows: [{ type: 'public' }] });
128+
// S3 HeadObject fails
79129
const notFoundError = new Error('Not Found');
80130
(notFoundError as any).name = 'NotFound';
81131
mockSend.mockRejectedValueOnce(notFoundError);
82132

83133
await expect(
84134
handler(
85135
{ file_id: '123', key: 'test.txt', bucket_id: 'bucket-123' },
86-
createMockContext()
136+
createMockContext({ pool, databaseId: 'db-123' })
87137
)
88138
).rejects.toThrow('File not found in S3');
89139
});
90140

91141
it('rethrows unexpected S3 errors', async () => {
92142
const handler = loadHandler();
143+
const pool = createMockPool();
144+
pool._mockQuery.mockResolvedValueOnce({ rows: [mockStorageModuleRow] });
145+
pool._mockQuery.mockResolvedValueOnce({ rows: [{ type: 'public' }] });
93146
mockSend.mockRejectedValueOnce(new Error('Network error'));
94147

95148
await expect(
96149
handler(
97150
{ file_id: '123', key: 'test.txt', bucket_id: 'bucket-123' },
98-
createMockContext()
151+
createMockContext({ pool, databaseId: 'db-123' })
99152
)
100153
).rejects.toThrow('Network error');
101154
});
@@ -104,95 +157,100 @@ describe('storage-confirm-upload handler', () => {
104157
describe('confirm upload', () => {
105158
it('calls confirm function when file exists in S3', async () => {
106159
const handler = loadHandler();
107-
mockSend.mockResolvedValueOnce({}); // HeadObject success
108-
mockPgQuery.mockResolvedValueOnce({ rows: [] }); // confirm_uploaded call
160+
const pool = createMockPool();
161+
// Storage module query
162+
pool._mockQuery.mockResolvedValueOnce({ rows: [mockStorageModuleRow] });
163+
// Bucket type query
164+
pool._mockQuery.mockResolvedValueOnce({ rows: [{ type: 'public' }] });
165+
// S3 HeadObject success
166+
mockSend.mockResolvedValueOnce({});
167+
// Confirm uploaded call
168+
pool._mockQuery.mockResolvedValueOnce({ rows: [] });
109169

110170
const result = await handler(
111-
{
112-
file_id: '123',
113-
key: 'test.txt',
114-
bucket_id: 'bucket-123',
115-
schema: 'test-db-app-public',
116-
table: 'app_files'
117-
},
118-
createMockContext()
171+
{ file_id: '123', key: 'test.txt', bucket_id: 'bucket-123' },
172+
createMockContext({ pool, databaseId: 'db-123' })
119173
);
120174

121175
expect(result.success).toBe(true);
122176
expect(result.file_id).toBe('123');
123-
expect(mockPgQuery).toHaveBeenCalled();
177+
// Verify confirm function was called with correct schema
178+
expect(pool._mockQuery).toHaveBeenCalledWith(
179+
expect.stringContaining('test-db-storage-private'),
180+
['123']
181+
);
182+
expect(pool._mockQuery).toHaveBeenCalledWith(
183+
expect.stringContaining('app_files_confirm_uploaded'),
184+
['123']
185+
);
124186
});
125187

126-
it('uses default schema and table when not provided', async () => {
188+
it('uses schema names from storage_module metadata', async () => {
127189
const handler = loadHandler();
128-
mockSend.mockResolvedValueOnce({}); // HeadObject success
129-
mockPgQuery.mockResolvedValueOnce({ rows: [] }); // confirm_uploaded call
190+
const pool = createMockPool();
191+
const customRow = {
192+
...mockStorageModuleRow,
193+
buckets_schema: 'custom-db-storage-public',
194+
files_schema: 'custom-db-storage-public',
195+
files_table: 'custom_files',
196+
};
197+
pool._mockQuery.mockResolvedValueOnce({ rows: [customRow] });
198+
pool._mockQuery.mockResolvedValueOnce({ rows: [{ type: 'private' }] });
199+
mockSend.mockResolvedValueOnce({});
200+
pool._mockQuery.mockResolvedValueOnce({ rows: [] });
130201

131202
const result = await handler(
132-
{
133-
file_id: '123',
134-
key: 'test.txt',
135-
bucket_id: 'bucket-123'
136-
},
137-
createMockContext()
203+
{ file_id: '456', key: 'doc.pdf', bucket_id: 'bucket-456' },
204+
createMockContext({ pool, databaseId: 'custom-db' })
138205
);
139206

140207
expect(result.success).toBe(true);
141-
// Should use default storage_public and app_files
142-
expect(mockPgQuery).toHaveBeenCalledWith(
143-
expect.stringContaining('app_files_confirm_uploaded'),
144-
['123']
208+
// Verify it used the custom schema/table from storage_module
209+
expect(pool._mockQuery).toHaveBeenCalledWith(
210+
expect.stringContaining('custom-db-storage-private'),
211+
['456']
212+
);
213+
expect(pool._mockQuery).toHaveBeenCalledWith(
214+
expect.stringContaining('custom_files_confirm_uploaded'),
215+
['456']
145216
);
146217
});
147218
});
148219

149220
describe('bucket name resolution', () => {
150-
it('resolves bucket name from database when schema and databaseId provided', async () => {
221+
it('resolves bucket name from database using storage_module schema', async () => {
151222
const handler = loadHandler();
152-
// First query: resolve bucket name
153-
mockPgQuery.mockResolvedValueOnce({ rows: [{ type: 'public' }] });
154-
// HeadObject success
223+
const pool = createMockPool();
224+
pool._mockQuery.mockResolvedValueOnce({ rows: [mockStorageModuleRow] });
225+
pool._mockQuery.mockResolvedValueOnce({ rows: [{ type: 'public' }] });
155226
mockSend.mockResolvedValueOnce({});
156-
// Second query: confirm_uploaded call
157-
mockPgQuery.mockResolvedValueOnce({ rows: [] });
158-
159-
const context = createMockContext();
160-
context.job.databaseId = 'db-123';
227+
pool._mockQuery.mockResolvedValueOnce({ rows: [] });
161228

162229
const result = await handler(
163-
{
164-
file_id: '123',
165-
key: 'test.txt',
166-
bucket_id: 'bucket-123',
167-
schema: 'test-db-app-public'
168-
},
169-
context
230+
{ file_id: '123', key: 'test.txt', bucket_id: 'bucket-123' },
231+
createMockContext({ pool, databaseId: 'db-123' })
170232
);
171233

172234
expect(result.success).toBe(true);
173235
expect(result.bucket).toBe('test-bucket-public-db-123');
236+
// Verify bucket query used correct schema from storage_module
237+
expect(pool._mockQuery).toHaveBeenCalledWith(
238+
expect.stringContaining('test-db-storage-public'),
239+
['bucket-123']
240+
);
174241
});
175242

176243
it('falls back to bucket_id when resolution fails', async () => {
177244
const handler = loadHandler();
178-
// First query fails
179-
mockPgQuery.mockRejectedValueOnce(new Error('DB error'));
180-
// HeadObject success
245+
const pool = createMockPool();
246+
pool._mockQuery.mockResolvedValueOnce({ rows: [mockStorageModuleRow] });
247+
pool._mockQuery.mockRejectedValueOnce(new Error('DB error'));
181248
mockSend.mockResolvedValueOnce({});
182-
// Second query: confirm_uploaded call
183-
mockPgQuery.mockResolvedValueOnce({ rows: [] });
184-
185-
const context = createMockContext();
186-
context.job.databaseId = 'db-123';
249+
pool._mockQuery.mockResolvedValueOnce({ rows: [] });
187250

188251
const result = await handler(
189-
{
190-
file_id: '123',
191-
key: 'test.txt',
192-
bucket_id: 'bucket-123',
193-
schema: 'test-db-app-public'
194-
},
195-
context
252+
{ file_id: '123', key: 'test.txt', bucket_id: 'bucket-123' },
253+
createMockContext({ pool, databaseId: 'db-123' })
196254
);
197255

198256
expect(result.success).toBe(true);
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
{
22
"name": "storage-confirm-upload",
33
"version": "1.0.0",
4-
"type": "node-graphql",
4+
"type": "node-sql",
55
"port": 8085,
66
"taskIdentifier": "storage:confirm_upload",
77
"description": "Confirms file upload by checking S3 HeadObject and transitioning status to uploaded",
88
"dependencies": {
99
"@aws-sdk/client-s3": "^3.1060.0",
10-
"@pgpmjs/env": "^2.15.3",
11-
"@pgpmjs/logger": "^2.4.3",
12-
"pg": "^8.16.0"
10+
"@pgsql/quotes": "^0.0.4"
1311
}
1412
}

0 commit comments

Comments
 (0)