Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c579543b8a | ||
| 0d84137786 | |||
|
|
20ee30c4b4 | ||
| 93612137e3 | |||
|
|
6e70f08e3c | ||
| 459f5f7976 | |||
|
|
a2e6331ddd | ||
| 13cd30bec9 | |||
|
|
baeb9488c6 | ||
| 0cba0f987e |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "flyer-crawler",
|
"name": "flyer-crawler",
|
||||||
"version": "0.9.1",
|
"version": "0.9.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "flyer-crawler",
|
"name": "flyer-crawler",
|
||||||
"version": "0.9.1",
|
"version": "0.9.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/api": "^6.14.2",
|
"@bull-board/api": "^6.14.2",
|
||||||
"@bull-board/express": "^6.14.2",
|
"@bull-board/express": "^6.14.2",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "flyer-crawler",
|
"name": "flyer-crawler",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.9.1",
|
"version": "0.9.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||||
|
|||||||
@@ -1079,6 +1079,8 @@ $$;
|
|||||||
-- It replaces the need to call get_best_sale_prices_for_user for each user individually.
|
-- It replaces the need to call get_best_sale_prices_for_user for each user individually.
|
||||||
-- Returns: TABLE(...) - A set of records including user details and deal information.
|
-- Returns: TABLE(...) - A set of records including user details and deal information.
|
||||||
-- =================================================================
|
-- =================================================================
|
||||||
|
DROP FUNCTION IF EXISTS public.get_best_sale_prices_for_all_users();
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION public.get_best_sale_prices_for_all_users()
|
CREATE OR REPLACE FUNCTION public.get_best_sale_prices_for_all_users()
|
||||||
RETURNS TABLE(
|
RETURNS TABLE(
|
||||||
user_id uuid,
|
user_id uuid,
|
||||||
|
|||||||
@@ -2809,6 +2809,8 @@ CREATE TRIGGER on_recipe_fork
|
|||||||
-- It replaces the need to call get_best_sale_prices_for_user for each user individually.
|
-- It replaces the need to call get_best_sale_prices_for_user for each user individually.
|
||||||
-- Returns: TABLE(...) - A set of records including user details and deal information.
|
-- Returns: TABLE(...) - A set of records including user details and deal information.
|
||||||
-- =================================================================
|
-- =================================================================
|
||||||
|
DROP FUNCTION IF EXISTS public.get_best_sale_prices_for_all_users();
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION public.get_best_sale_prices_for_all_users()
|
CREATE OR REPLACE FUNCTION public.get_best_sale_prices_for_all_users()
|
||||||
RETURNS TABLE(
|
RETURNS TABLE(
|
||||||
user_id uuid,
|
user_id uuid,
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ async function main() {
|
|||||||
validTo.setDate(today.getDate() + 5);
|
validTo.setDate(today.getDate() + 5);
|
||||||
|
|
||||||
const flyerQuery = `
|
const flyerQuery = `
|
||||||
INSERT INTO public.flyers (file_name, image_url, checksum, store_id, valid_from, valid_to)
|
INSERT INTO public.flyers (file_name, image_url, icon_url, checksum, store_id, valid_from, valid_to)
|
||||||
VALUES ('safeway-flyer.jpg', 'https://example.com/flyer-images/safeway-flyer.jpg', 'a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0', ${storeMap.get('Safeway')}, $1, $2)
|
VALUES ('safeway-flyer.jpg', 'https://example.com/flyer-images/safeway-flyer.jpg', 'https://example.com/flyer-images/icons/safeway-flyer.jpg', 'a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0', ${storeMap.get('Safeway')}, $1, $2)
|
||||||
RETURNING flyer_id;
|
RETURNING flyer_id;
|
||||||
`;
|
`;
|
||||||
const flyerRes = await client.query<{ flyer_id: number }>(flyerQuery, [
|
const flyerRes = await client.query<{ flyer_id: number }>(flyerQuery, [
|
||||||
|
|||||||
@@ -279,8 +279,8 @@ describe('AI Service (Server)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Check second call
|
// Check second call
|
||||||
expect(mockGenerateContent).toHaveBeenNthCalledWith(2, { // The second model in the list is 'gemini-2.5-flash'
|
expect(mockGenerateContent).toHaveBeenNthCalledWith(2, { // The second model in the list is 'gemini-2.5-pro'
|
||||||
model: 'gemini-2.5-flash',
|
model: 'gemini-2.5-pro',
|
||||||
...request,
|
...request,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ describe('FlyerDataTransformer', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
transformer = new FlyerDataTransformer();
|
transformer = new FlyerDataTransformer();
|
||||||
|
// Stub environment variables to ensure consistency and predictability.
|
||||||
|
// Prioritize FRONTEND_URL to match the updated service logic.
|
||||||
|
vi.stubEnv('FRONTEND_URL', 'http://localhost:3000');
|
||||||
|
vi.stubEnv('BASE_URL', ''); // Ensure this is not used to confirm priority logic
|
||||||
|
vi.stubEnv('PORT', ''); // Ensure this is not used
|
||||||
|
|
||||||
// Provide a default mock implementation for generateFlyerIcon
|
// Provide a default mock implementation for generateFlyerIcon
|
||||||
vi.mocked(generateFlyerIcon).mockResolvedValue('icon-flyer-page-1.webp');
|
vi.mocked(generateFlyerIcon).mockResolvedValue('icon-flyer-page-1.webp');
|
||||||
@@ -70,6 +75,9 @@ describe('FlyerDataTransformer', () => {
|
|||||||
mockLogger,
|
mockLogger,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Dynamically construct the expected base URL, mirroring the logic in the transformer.
|
||||||
|
const expectedBaseUrl = `http://localhost:3000`;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
// 0. Check logging
|
// 0. Check logging
|
||||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||||
@@ -83,8 +91,8 @@ describe('FlyerDataTransformer', () => {
|
|||||||
// 1. Check flyer data
|
// 1. Check flyer data
|
||||||
expect(flyerData).toEqual({
|
expect(flyerData).toEqual({
|
||||||
file_name: originalFileName,
|
file_name: originalFileName,
|
||||||
image_url: `http://localhost:3000/flyer-images/flyer-page-1.jpg`,
|
image_url: `${expectedBaseUrl}/flyer-images/flyer-page-1.jpg`,
|
||||||
icon_url: `http://localhost:3000/flyer-images/icons/icon-flyer-page-1.webp`,
|
icon_url: `${expectedBaseUrl}/flyer-images/icons/icon-flyer-page-1.webp`,
|
||||||
checksum,
|
checksum,
|
||||||
store_name: 'Test Store',
|
store_name: 'Test Store',
|
||||||
valid_from: '2024-01-01',
|
valid_from: '2024-01-01',
|
||||||
@@ -151,6 +159,9 @@ describe('FlyerDataTransformer', () => {
|
|||||||
mockLogger,
|
mockLogger,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Dynamically construct the expected base URL, mirroring the logic in the transformer.
|
||||||
|
const expectedBaseUrl = `http://localhost:3000`;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
// 0. Check logging
|
// 0. Check logging
|
||||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||||
@@ -167,8 +178,8 @@ describe('FlyerDataTransformer', () => {
|
|||||||
expect(itemsForDb).toHaveLength(0);
|
expect(itemsForDb).toHaveLength(0);
|
||||||
expect(flyerData).toEqual({
|
expect(flyerData).toEqual({
|
||||||
file_name: originalFileName,
|
file_name: originalFileName,
|
||||||
image_url: `http://localhost:3000/flyer-images/another.png`,
|
image_url: `${expectedBaseUrl}/flyer-images/another.png`,
|
||||||
icon_url: `http://localhost:3000/flyer-images/icons/icon-another.webp`,
|
icon_url: `${expectedBaseUrl}/flyer-images/icons/icon-another.webp`,
|
||||||
checksum,
|
checksum,
|
||||||
store_name: 'Unknown Store (auto)', // Should use fallback
|
store_name: 'Unknown Store (auto)', // Should use fallback
|
||||||
valid_from: null,
|
valid_from: null,
|
||||||
|
|||||||
@@ -76,19 +76,25 @@ export class FlyerDataTransformer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Construct proper URLs including protocol and host to satisfy DB constraints
|
// Construct proper URLs including protocol and host to satisfy DB constraints
|
||||||
const baseUrl = process.env.BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
|
const rawBaseUrl = process.env.FRONTEND_URL || process.env.BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
|
||||||
|
// Normalize base URL by removing any trailing slash to prevent double slashes in the final URL,
|
||||||
|
// and replace the strict `new URL()` constructor to prevent exceptions in test environments.
|
||||||
|
const baseUrl = rawBaseUrl.endsWith('/') ? rawBaseUrl.slice(0, -1) : rawBaseUrl;
|
||||||
|
|
||||||
const flyerData: FlyerInsert = {
|
const flyerData: FlyerInsert = {
|
||||||
file_name: originalFileName,
|
file_name: originalFileName,
|
||||||
image_url: new URL(`/flyer-images/${path.basename(firstImage)}`, baseUrl).href,
|
image_url: `${baseUrl}/flyer-images/${path.basename(firstImage)}`,
|
||||||
icon_url: new URL(`/flyer-images/icons/${iconFileName}`, baseUrl).href,
|
icon_url: `${baseUrl}/flyer-images/icons/${iconFileName}`,
|
||||||
checksum,
|
checksum,
|
||||||
store_name: storeName,
|
store_name: storeName,
|
||||||
valid_from: extractedData.valid_from,
|
valid_from: extractedData.valid_from,
|
||||||
valid_to: extractedData.valid_to,
|
valid_to: extractedData.valid_to,
|
||||||
store_address: extractedData.store_address, // The number of items is now calculated directly from the transformed data.
|
store_address: extractedData.store_address, // The number of items is now calculated directly from the transformed data.
|
||||||
item_count: itemsForDb.length,
|
item_count: itemsForDb.length,
|
||||||
uploaded_by: userId,
|
// Defensively handle the userId. An empty string ('') is not a valid UUID,
|
||||||
|
// but `null` is. This ensures that any falsy value for userId (undefined, null, '')
|
||||||
|
// is converted to `null` for the database, preventing a 22P02 error.
|
||||||
|
uploaded_by: userId || null,
|
||||||
status: needsReview ? 'needs_review' : 'processed',
|
status: needsReview ? 'needs_review' : 'processed',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,10 @@ describe('Authentication E2E Flow', () => {
|
|||||||
expect(registerResponse.status).toBe(201);
|
expect(registerResponse.status).toBe(201);
|
||||||
createdUserIds.push(registerData.userprofile.user.user_id);
|
createdUserIds.push(registerData.userprofile.user.user_id);
|
||||||
|
|
||||||
|
// Add a small delay to mitigate potential DB replication lag or race conditions
|
||||||
|
// where the user might not be found immediately after creation.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
// Act 1: Request a password reset.
|
// Act 1: Request a password reset.
|
||||||
// The test environment returns the token directly in the response for E2E testing.
|
// The test environment returns the token directly in the response for E2E testing.
|
||||||
const forgotResponse = await apiClient.requestPasswordReset(email);
|
const forgotResponse = await apiClient.requestPasswordReset(email);
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ describe('E2E Flyer Upload and Processing Workflow', () => {
|
|||||||
|
|
||||||
// 5. Poll for job completion
|
// 5. Poll for job completion
|
||||||
let jobStatus;
|
let jobStatus;
|
||||||
const maxRetries = 30; // Poll for up to 90 seconds
|
const maxRetries = 60; // Poll for up to 180 seconds
|
||||||
for (let i = 0; i < maxRetries; i++) {
|
for (let i = 0; i < maxRetries; i++) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 3000)); // Wait 3s
|
await new Promise((resolve) => setTimeout(resolve, 3000)); // Wait 3s
|
||||||
|
|
||||||
@@ -106,5 +106,5 @@ describe('E2E Flyer Upload and Processing Workflow', () => {
|
|||||||
expect(jobStatus.state).toBe('completed');
|
expect(jobStatus.state).toBe('completed');
|
||||||
flyerId = jobStatus.returnValue?.flyerId;
|
flyerId = jobStatus.returnValue?.flyerId;
|
||||||
expect(flyerId).toBeTypeOf('number');
|
expect(flyerId).toBeTypeOf('number');
|
||||||
}, 120000); // Extended timeout for AI processing
|
}, 240000); // Extended timeout for AI processing
|
||||||
});
|
});
|
||||||
@@ -163,8 +163,8 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
// Before each modification test, create a fresh flyer item and a correction for it.
|
// Before each modification test, create a fresh flyer item and a correction for it.
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const flyerRes = await getPool().query(
|
const flyerRes = await getPool().query(
|
||||||
`INSERT INTO public.flyers (store_id, file_name, image_url, item_count, checksum)
|
`INSERT INTO public.flyers (store_id, file_name, image_url, icon_url, item_count, checksum)
|
||||||
VALUES ($1, 'admin-test.jpg', 'https://example.com/flyer-images/asdmin-test.jpg', 1, $2) RETURNING flyer_id`,
|
VALUES ($1, 'admin-test.jpg', 'https://example.com/flyer-images/asdmin-test.jpg', 'https://example.com/flyer-images/icons/admin-test.jpg', 1, $2) RETURNING flyer_id`,
|
||||||
// The checksum must be a unique 64-character string to satisfy the DB constraint.
|
// The checksum must be a unique 64-character string to satisfy the DB constraint.
|
||||||
// We generate a dynamic string and pad it to 64 characters.
|
// We generate a dynamic string and pad it to 64 characters.
|
||||||
[testStoreId, `checksum-${Date.now()}-${Math.random()}`.padEnd(64, '0')],
|
[testStoreId, `checksum-${Date.now()}-${Math.random()}`.padEnd(64, '0')],
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
|||||||
|
|
||||||
// Poll for job completion
|
// Poll for job completion
|
||||||
let jobStatus;
|
let jobStatus;
|
||||||
const maxRetries = 30; // Poll for up to 90 seconds
|
const maxRetries = 60; // Poll for up to 180 seconds
|
||||||
for (let i = 0; i < maxRetries; i++) {
|
for (let i = 0; i < maxRetries; i++) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||||
const statusResponse = await request
|
const statusResponse = await request
|
||||||
@@ -309,7 +309,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
|||||||
|
|
||||||
// Poll for job completion
|
// Poll for job completion
|
||||||
let jobStatus;
|
let jobStatus;
|
||||||
const maxRetries = 30;
|
const maxRetries = 60; // Poll for up to 180 seconds
|
||||||
for (let i = 0; i < maxRetries; i++) {
|
for (let i = 0; i < maxRetries; i++) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||||
const statusResponse = await request
|
const statusResponse = await request
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ describe('Public Flyer API Routes Integration Tests', () => {
|
|||||||
const storeId = storeRes.rows[0].store_id;
|
const storeId = storeRes.rows[0].store_id;
|
||||||
|
|
||||||
const flyerRes = await getPool().query(
|
const flyerRes = await getPool().query(
|
||||||
`INSERT INTO public.flyers (store_id, file_name, image_url, item_count, checksum)
|
`INSERT INTO public.flyers (store_id, file_name, image_url, icon_url, item_count, checksum)
|
||||||
VALUES ($1, 'integration-test.jpg', 'https://example.com/flyer-images/integration-test.jpg', 1, $2) RETURNING flyer_id`,
|
VALUES ($1, 'integration-test.jpg', 'https://example.com/flyer-images/integration-test.jpg', 'https://example.com/flyer-images/icons/integration-test.jpg', 1, $2) RETURNING flyer_id`,
|
||||||
[storeId, `${Date.now().toString(16)}`.padEnd(64, '0')],
|
[storeId, `${Date.now().toString(16)}`.padEnd(64, '0')],
|
||||||
);
|
);
|
||||||
createdFlyerId = flyerRes.rows[0].flyer_id;
|
createdFlyerId = flyerRes.rows[0].flyer_id;
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ describe('Gamification Flow Integration Test', () => {
|
|||||||
|
|
||||||
// --- Act 2: Poll for job completion ---
|
// --- Act 2: Poll for job completion ---
|
||||||
let jobStatus;
|
let jobStatus;
|
||||||
const maxRetries = 30; // Poll for up to 90 seconds
|
const maxRetries = 60; // Poll for up to 180 seconds
|
||||||
for (let i = 0; i < maxRetries; i++) {
|
for (let i = 0; i < maxRetries; i++) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||||
const statusResponse = await request
|
const statusResponse = await request
|
||||||
@@ -162,6 +162,6 @@ describe('Gamification Flow Integration Test', () => {
|
|||||||
firstUploadAchievement!.points_value,
|
firstUploadAchievement!.points_value,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
120000, // Increase timeout to 120 seconds for this long-running test
|
240000, // Increase timeout to 240s to match other long-running processing tests
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -34,22 +34,22 @@ describe('Price History API Integration Test (/api/price-history)', () => {
|
|||||||
|
|
||||||
// 3. Create two flyers with different dates
|
// 3. Create two flyers with different dates
|
||||||
const flyerRes1 = await pool.query(
|
const flyerRes1 = await pool.query(
|
||||||
`INSERT INTO public.flyers (store_id, file_name, image_url, item_count, checksum, valid_from)
|
`INSERT INTO public.flyers (store_id, file_name, image_url, icon_url, item_count, checksum, valid_from)
|
||||||
VALUES ($1, 'price-test-1.jpg', 'https://example.com/flyer-images/price-test-1.jpg', 1, $2, '2025-01-01') RETURNING flyer_id`,
|
VALUES ($1, 'price-test-1.jpg', 'https://example.com/flyer-images/price-test-1.jpg', 'https://example.com/flyer-images/icons/price-test-1.jpg', 1, $2, '2025-01-01') RETURNING flyer_id`,
|
||||||
[storeId, `${Date.now().toString(16)}1`.padEnd(64, '0')],
|
[storeId, `${Date.now().toString(16)}1`.padEnd(64, '0')],
|
||||||
);
|
);
|
||||||
flyerId1 = flyerRes1.rows[0].flyer_id;
|
flyerId1 = flyerRes1.rows[0].flyer_id;
|
||||||
|
|
||||||
const flyerRes2 = await pool.query(
|
const flyerRes2 = await pool.query(
|
||||||
`INSERT INTO public.flyers (store_id, file_name, image_url, item_count, checksum, valid_from)
|
`INSERT INTO public.flyers (store_id, file_name, image_url, icon_url, item_count, checksum, valid_from)
|
||||||
VALUES ($1, 'price-test-2.jpg', 'https://example.com/flyer-images/price-test-2.jpg', 1, $2, '2025-01-08') RETURNING flyer_id`,
|
VALUES ($1, 'price-test-2.jpg', 'https://example.com/flyer-images/price-test-2.jpg', 'https://example.com/flyer-images/icons/price-test-2.jpg', 1, $2, '2025-01-08') RETURNING flyer_id`,
|
||||||
[storeId, `${Date.now().toString(16)}2`.padEnd(64, '0')],
|
[storeId, `${Date.now().toString(16)}2`.padEnd(64, '0')],
|
||||||
);
|
);
|
||||||
flyerId2 = flyerRes2.rows[0].flyer_id; // This was a duplicate, fixed.
|
flyerId2 = flyerRes2.rows[0].flyer_id; // This was a duplicate, fixed.
|
||||||
|
|
||||||
const flyerRes3 = await pool.query(
|
const flyerRes3 = await pool.query(
|
||||||
`INSERT INTO public.flyers (store_id, file_name, image_url, item_count, checksum, valid_from)
|
`INSERT INTO public.flyers (store_id, file_name, image_url, icon_url, item_count, checksum, valid_from)
|
||||||
VALUES ($1, 'price-test-3.jpg', 'https://example.com/flyer-images/price-test-3.jpg', 1, $2, '2025-01-15') RETURNING flyer_id`,
|
VALUES ($1, 'price-test-3.jpg', 'https://example.com/flyer-images/price-test-3.jpg', 'https://example.com/flyer-images/icons/price-test-3.jpg', 1, $2, '2025-01-15') RETURNING flyer_id`,
|
||||||
[storeId, `${Date.now().toString(16)}3`.padEnd(64, '0')],
|
[storeId, `${Date.now().toString(16)}3`.padEnd(64, '0')],
|
||||||
);
|
);
|
||||||
flyerId3 = flyerRes3.rows[0].flyer_id;
|
flyerId3 = flyerRes3.rows[0].flyer_id;
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ describe('Public API Routes Integration Tests', () => {
|
|||||||
);
|
);
|
||||||
testStoreId = storeRes.rows[0].store_id;
|
testStoreId = storeRes.rows[0].store_id;
|
||||||
const flyerRes = await pool.query(
|
const flyerRes = await pool.query(
|
||||||
`INSERT INTO public.flyers (store_id, file_name, image_url, item_count, checksum)
|
`INSERT INTO public.flyers (store_id, file_name, image_url, icon_url, item_count, checksum)
|
||||||
VALUES ($1, 'public-routes-test.jpg', 'https://example.com/flyer-images/public-routes-test.jpg', 1, $2) RETURNING *`,
|
VALUES ($1, 'public-routes-test.jpg', 'https://example.com/flyer-images/public-routes-test.jpg', 'https://example.com/flyer-images/icons/public-routes-test.jpg', 1, $2) RETURNING *`,
|
||||||
[testStoreId, `${Date.now().toString(16)}`.padEnd(64, '0')],
|
[testStoreId, `${Date.now().toString(16)}`.padEnd(64, '0')],
|
||||||
);
|
);
|
||||||
testFlyer = flyerRes.rows[0];
|
testFlyer = flyerRes.rows[0];
|
||||||
|
|||||||
@@ -151,7 +151,6 @@ export interface User {
|
|||||||
*/
|
*/
|
||||||
export interface UserWithPasswordHash extends User {
|
export interface UserWithPasswordHash extends User {
|
||||||
password_hash: string | null;
|
password_hash: string | null;
|
||||||
refresh_token: string | null;
|
|
||||||
readonly failed_login_attempts: number;
|
readonly failed_login_attempts: number;
|
||||||
readonly last_failed_login: string | null; // TIMESTAMPTZ
|
readonly last_failed_login: string | null; // TIMESTAMPTZ
|
||||||
readonly last_login_at?: string | null; // TIMESTAMPTZ
|
readonly last_login_at?: string | null; // TIMESTAMPTZ
|
||||||
|
|||||||
Reference in New Issue
Block a user