diff --git a/Makefile b/Makefile index 78d5994c..b2b326b3 100644 --- a/Makefile +++ b/Makefile @@ -9,4 +9,4 @@ ssh: docker exec -it postgres /bin/bash install: - docker exec postgres /sql-bin/install.sh + docker exec postgres /sql-bin/install.sh \ No newline at end of file diff --git a/packages/data-types/types/README.md b/packages/data-types/types/README.md index 2cda9d94..37c7e6cf 100644 --- a/packages/data-types/types/README.md +++ b/packages/data-types/types/README.md @@ -158,6 +158,7 @@ INSERT INTO customers (domain) VALUES ### Image and Attachment Domains The `image` domain stores JSON objects with URL and MIME type information. The `attachment` domain accepts either that JSON shape or a plain URL string. +The `upload` domain uses the same JSON object shape as `image`, ensuring both the file URL and MIME type are present. ```sql -- Valid image @@ -184,7 +185,7 @@ INSERT INTO customers (document) VALUES ('https://storage.example.com/favicon.ic | `hostname` | `text` | Domain name without protocol | `example.com` | | `image` | `json` | Image metadata with URL and MIME | `{"url": "...", "mime": "image/jpeg"}` | | `attachment` | `json` | File attachment URL or metadata | `{"url": "...", "mime": "application/pdf"}` or `https://example.com/favicon.ico` | -| `upload` | `text` | File upload identifier | Various formats | +| `upload` | `json` | File upload metadata (URL + MIME) | `{"url": "...", "mime": "application/pdf"}` | | `single_select` | `text` | Single selection value | Text value | | `multiple_select` | `text[]` | Multiple selection values | Array of text values | @@ -212,7 +213,7 @@ The test suite validates: - Email format validation (valid and invalid cases) - URL format validation with extensive test cases - Hostname format validation -- Image and attachment JSON structure validation +- Image, upload, and attachment JSON structure validation ## Related Tooling diff --git a/packages/data-types/types/__tests__/domains.pgutils.test.ts b/packages/data-types/types/__tests__/domains.pgutils.test.ts index 6be02423..7e01d98f 100644 --- a/packages/data-types/types/__tests__/domains.pgutils.test.ts +++ b/packages/data-types/types/__tests__/domains.pgutils.test.ts @@ -64,6 +64,17 @@ const invalidImages = [ { url: 'https://foo.bar/some.png' } ]; +const validUploads = [ + { url: 'http://www.foo.bar/some.jpg', mime: 'image/jpg' }, + { url: 'https://foo.bar/some.PNG', mime: 'image/png' } +]; + +const invalidUploads = [ + { url: 'hi there' }, + { url: 'https://foo.bar/some.png' }, + { url: 'ftp://foo.bar/some.png', mime: 'image/png' } +]; + let pg: PgTestClient; let teardown: () => Promise; @@ -79,7 +90,8 @@ CREATE TABLE customers ( image image, attachment attachment, domain hostname, - email email + email email, + upload upload ); `); }); @@ -129,6 +141,24 @@ describe('types', () => { } }); + it('valid upload', async () => { + for (const upload of validUploads) { + await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); + } + }); + + it('invalid upload', async () => { + for (const upload of invalidUploads) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); + } + }); + it('valid url', async () => { for (const value of validUrls) { await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); diff --git a/packages/data-types/types/__tests__/domains.test.ts b/packages/data-types/types/__tests__/domains.test.ts index 6be02423..7e01d98f 100644 --- a/packages/data-types/types/__tests__/domains.test.ts +++ b/packages/data-types/types/__tests__/domains.test.ts @@ -64,6 +64,17 @@ const invalidImages = [ { url: 'https://foo.bar/some.png' } ]; +const validUploads = [ + { url: 'http://www.foo.bar/some.jpg', mime: 'image/jpg' }, + { url: 'https://foo.bar/some.PNG', mime: 'image/png' } +]; + +const invalidUploads = [ + { url: 'hi there' }, + { url: 'https://foo.bar/some.png' }, + { url: 'ftp://foo.bar/some.png', mime: 'image/png' } +]; + let pg: PgTestClient; let teardown: () => Promise; @@ -79,7 +90,8 @@ CREATE TABLE customers ( image image, attachment attachment, domain hostname, - email email + email email, + upload upload ); `); }); @@ -129,6 +141,24 @@ describe('types', () => { } }); + it('valid upload', async () => { + for (const upload of validUploads) { + await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); + } + }); + + it('invalid upload', async () => { + for (const upload of invalidUploads) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); + } + }); + it('valid url', async () => { for (const value of validUrls) { await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); diff --git a/packages/data-types/types/deploy/schemas/public/domains/upload.sql b/packages/data-types/types/deploy/schemas/public/domains/upload.sql index 09a94337..0af945bc 100644 --- a/packages/data-types/types/deploy/schemas/public/domains/upload.sql +++ b/packages/data-types/types/deploy/schemas/public/domains/upload.sql @@ -4,7 +4,11 @@ BEGIN; -CREATE DOMAIN upload AS text CHECK (VALUE ~ '^(https?)://[^\s/$.?#].[^\s]*$'); +CREATE DOMAIN upload AS jsonb CHECK ( + value ?& ARRAY['url', 'mime'] + AND + value->>'url' ~ '^(https?)://[^\s/$.?#].[^\s]*$' +); COMMENT ON DOMAIN upload IS E'@name launchqlInternalTypeUpload'; COMMIT; diff --git a/packages/data-types/types/sql/launchql-types--0.9.0.sql b/packages/data-types/types/sql/launchql-types--0.9.0.sql index 6eecade0..ce5cef44 100644 --- a/packages/data-types/types/sql/launchql-types--0.9.0.sql +++ b/packages/data-types/types/sql/launchql-types--0.9.0.sql @@ -35,12 +35,15 @@ CREATE DOMAIN single_select AS jsonb COMMENT ON DOMAIN single_select IS '@name launchqlInternalTypeSingleSelect'; -CREATE DOMAIN upload AS text - CHECK (value ~ E'^(https?)://[^\\s/$.?#].[^\\s]*$'); +CREATE DOMAIN upload AS jsonb + CHECK ( + value ?& ARRAY['url', 'mime'] + AND (value ->> 'url') ~ E'^(https?)://[^\\s/$.?#].[^\\s]*$' +); COMMENT ON DOMAIN upload IS '@name launchqlInternalTypeUpload'; CREATE DOMAIN url AS text CHECK (value ~ E'^(https?)://[^\\s/$.?#].[^\\s]*$'); -COMMENT ON DOMAIN url IS '@name launchqlInternalTypeUrl'; \ No newline at end of file +COMMENT ON DOMAIN url IS '@name launchqlInternalTypeUrl'; diff --git a/packages/metrics/achievements/jest.config.js b/packages/metrics/achievements/jest.config.js index e20e7efb..db566b12 100644 --- a/packages/metrics/achievements/jest.config.js +++ b/packages/metrics/achievements/jest.config.js @@ -2,6 +2,7 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', + maxWorkers: 1, // Match both __tests__ and colocated test files testMatch: ['**/?(*.)+(test|spec).{ts,tsx,js,jsx}'],