From 27603ef94bf7738d8c3bca8fc819f245f1f7f2bd Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Fri, 7 Aug 2026 23:04:21 +0000 Subject: [PATCH] feat(types): declare bucket_id, size and filename on the upload and image domains The documents are projections of a files row, so the contract now constrains the keys the platform actually resolves storage with. bucket_id must parse as a canonical uuid; size and filename get json type checks. Every key stays optional, so url-only external references keep validating. --- packages/types/__tests__/domains.test.ts | 63 +++++++++++++++++- .../public/domains/image_file_ref_keys.sql | 23 +++++++ .../public/domains/upload_file_ref_keys.sql | 22 ++++++ packages/types/pgpm.plan | 2 + .../public/domains/image_file_ref_keys.sql | 19 ++++++ .../public/domains/upload_file_ref_keys.sql | 18 +++++ .../sql/pgpm-types--0.39.0.bundle.tar.gz | Bin 3335 -> 4488 bytes packages/types/sql/pgpm-types--0.39.0.sql | 60 ++++++++++++++++- .../public/domains/image_file_ref_keys.sql | 29 ++++++++ .../public/domains/upload_file_ref_keys.sql | 29 ++++++++ 10 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 packages/types/deploy/schemas/public/domains/image_file_ref_keys.sql create mode 100644 packages/types/deploy/schemas/public/domains/upload_file_ref_keys.sql create mode 100644 packages/types/revert/schemas/public/domains/image_file_ref_keys.sql create mode 100644 packages/types/revert/schemas/public/domains/upload_file_ref_keys.sql create mode 100644 packages/types/verify/schemas/public/domains/image_file_ref_keys.sql create mode 100644 packages/types/verify/schemas/public/domains/upload_file_ref_keys.sql diff --git a/packages/types/__tests__/domains.test.ts b/packages/types/__tests__/domains.test.ts index 6594f302..290ad530 100644 --- a/packages/types/__tests__/domains.test.ts +++ b/packages/types/__tests__/domains.test.ts @@ -8,6 +8,10 @@ import { getConnections, PgTestClient } from 'pgsql-test'; // - email: must contain @ (value ~ '@') // - image: jsonb object requiring 'url' OR 'id' OR 'key', with type validation, optional bucket/provider/mime/versions (versions is array) // - upload: jsonb object requiring 'url' OR 'id' OR 'key', with type validation on all fields, optional bucket/provider/mime +// both also carry the files-row projection keys: bucket_id (uuid string), size (number), filename (string) + +const fileId = '0d1e3d64-1e2a-4c7f-9c3a-6f7f9f2b1c44'; +const bucketId = '9a7f1c2e-4b6d-4a11-8f30-2c5d7e9a0b13'; const validUrls = [ 'http://foo.com/blah_blah', @@ -67,7 +71,9 @@ const validImages = [ { key: 'some-image-key' }, { id: 'private-image', bucket: 'my-bucket', provider: 's3' }, { url: 'https://example.com/image.png', versions: ['thumb', 'medium', 'large'] }, - { id: 'image-with-versions', versions: [{ size: 'thumb' }, { size: 'large' }] } + { id: 'image-with-versions', versions: [{ size: 'thumb' }, { size: 'large' }] }, + { id: fileId, key: 'abc', mime: 'image/png', bucket_id: bucketId, size: 12345, filename: 'hero.png' }, + { id: fileId, bucket_id: bucketId.toUpperCase() } ]; const invalidImages = [ @@ -81,6 +87,10 @@ const invalidImages = [ { url: 'https://example.com/image.png', provider: true }, { url: 'https://example.com/image.png', mime: ['array'] }, { url: 'https://example.com/image.png', versions: 'not-an-array' }, + { id: fileId, bucket_id: 'not-a-uuid' }, + { id: fileId, bucket_id: bucketId.replace(/-/g, '') }, + { id: fileId, size: '12345' }, + { id: fileId, filename: 42 }, 'not-an-object', ['array-not-object'] ]; @@ -93,7 +103,9 @@ const validUploads = [ { url: 'https://example.com/file.pdf', id: 'with-id' }, { id: 'some-id', bucket: 'my-bucket', provider: 's3' }, { key: 'some-key', mime: 'application/pdf' }, - { url: 'https://example.com/file.pdf', bucket: 'bucket', provider: 'gcs', mime: 'application/pdf' } + { url: 'https://example.com/file.pdf', bucket: 'bucket', provider: 'gcs', mime: 'application/pdf' }, + { id: fileId, key: 'abc', mime: 'application/pdf', bucket_id: bucketId, size: 12345, filename: 'contract.pdf' }, + { id: fileId, bucket_id: bucketId.toUpperCase() } ]; const invalidUploads = [ @@ -106,6 +118,10 @@ const invalidUploads = [ { url: 'https://example.com/file.pdf', bucket: 123 }, { url: 'https://example.com/file.pdf', provider: ['array'] }, { id: 'some-id', mime: { nested: 'object' } }, + { id: fileId, bucket_id: 'not-a-uuid' }, + { id: fileId, bucket_id: bucketId.replace(/-/g, '') }, + { id: fileId, size: '12345' }, + { id: fileId, filename: 42 }, 'not-an-object', ['array-not-object'] ]; @@ -317,4 +333,47 @@ describe('types', () => { } }); }); + + describe.each(['upload', 'image'])('%s files-row projection keys', (column) => { + const insert = (value: unknown) => + pg.any(`INSERT INTO customers (${column}) VALUES ($1::json);`, [value]); + + it('accepts the full projection of a files row', async () => { + await insert({ + id: fileId, + key: 'e3b0c44298fc1c149afbf4c8996fb924', + mime: 'image/png', + bucket_id: bucketId, + size: 12345, + filename: 'hero.png' + }); + }); + + it('rejects a bucket_id that is not a uuid', async () => { + await expect(insert({ id: fileId, bucket_id: 'not-a-uuid' })).rejects.toThrow(); + await expect(insert({ id: fileId, bucket_id: '' })).rejects.toThrow(); + await expect(insert({ id: fileId, bucket_id: `${bucketId}-extra` })).rejects.toThrow(); + await expect(insert({ id: fileId, bucket_id: bucketId.replace(/-/g, '') })).rejects.toThrow(); + }); + + it('rejects a bucket_id that is not a json string', async () => { + await expect(insert({ id: fileId, bucket_id: 12345 })).rejects.toThrow(); + await expect(insert({ id: fileId, bucket_id: { uuid: bucketId } })).rejects.toThrow(); + }); + + it('keeps url-only external references valid', async () => { + await insert({ url: 'https://gravatar.com/avatar/abc', mime: 'image/png' }); + }); + + it('leaves the new keys optional', async () => { + await insert({ id: fileId }); + await insert({ key: 'some-key' }); + }); + + it('constrains size to a number and filename to a string', async () => { + await insert({ id: fileId, size: 0, filename: 'a.txt' }); + await expect(insert({ id: fileId, size: '12345' })).rejects.toThrow(); + await expect(insert({ id: fileId, filename: 42 })).rejects.toThrow(); + }); + }); }); diff --git a/packages/types/deploy/schemas/public/domains/image_file_ref_keys.sql b/packages/types/deploy/schemas/public/domains/image_file_ref_keys.sql new file mode 100644 index 00000000..d6b38b4e --- /dev/null +++ b/packages/types/deploy/schemas/public/domains/image_file_ref_keys.sql @@ -0,0 +1,23 @@ +-- Deploy schemas/public/domains/image_file_ref_keys to pg +-- requires: schemas/public/domains/image + +BEGIN; + +ALTER DOMAIN image DROP CONSTRAINT image_check; + +ALTER DOMAIN image ADD CONSTRAINT image_check CHECK ( + jsonb_typeof(value) = 'object' + AND (value ? 'url' OR value ? 'id' OR value ? 'key') + AND (NOT value ? 'url' OR (value->>'url') ~ '^https?://[^\s]+$') + AND (NOT value ? 'id' OR jsonb_typeof(value->'id') = 'string') + AND (NOT value ? 'key' OR jsonb_typeof(value->'key') = 'string') + AND (NOT value ? 'bucket' OR jsonb_typeof(value->'bucket') = 'string') + AND (NOT value ? 'bucket_id' OR (value->>'bucket_id') ~ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') + AND (NOT value ? 'provider' OR jsonb_typeof(value->'provider') = 'string') + AND (NOT value ? 'mime' OR jsonb_typeof(value->'mime') = 'string') + AND (NOT value ? 'size' OR jsonb_typeof(value->'size') = 'number') + AND (NOT value ? 'filename' OR jsonb_typeof(value->'filename') = 'string') + AND (NOT value ? 'versions' OR jsonb_typeof(value->'versions') = 'array') +); + +COMMIT; diff --git a/packages/types/deploy/schemas/public/domains/upload_file_ref_keys.sql b/packages/types/deploy/schemas/public/domains/upload_file_ref_keys.sql new file mode 100644 index 00000000..ac707732 --- /dev/null +++ b/packages/types/deploy/schemas/public/domains/upload_file_ref_keys.sql @@ -0,0 +1,22 @@ +-- Deploy schemas/public/domains/upload_file_ref_keys to pg +-- requires: schemas/public/domains/upload + +BEGIN; + +ALTER DOMAIN upload DROP CONSTRAINT upload_check; + +ALTER DOMAIN upload ADD CONSTRAINT upload_check CHECK ( + jsonb_typeof(value) = 'object' + AND (value ? 'url' OR value ? 'id' OR value ? 'key') + AND (NOT value ? 'url' OR (value->>'url') ~ '^https?://[^\s]+$') + AND (NOT value ? 'id' OR jsonb_typeof(value->'id') = 'string') + AND (NOT value ? 'key' OR jsonb_typeof(value->'key') = 'string') + AND (NOT value ? 'bucket' OR jsonb_typeof(value->'bucket') = 'string') + AND (NOT value ? 'bucket_id' OR (value->>'bucket_id') ~ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') + AND (NOT value ? 'provider' OR jsonb_typeof(value->'provider') = 'string') + AND (NOT value ? 'mime' OR jsonb_typeof(value->'mime') = 'string') + AND (NOT value ? 'size' OR jsonb_typeof(value->'size') = 'number') + AND (NOT value ? 'filename' OR jsonb_typeof(value->'filename') = 'string') +); + +COMMIT; diff --git a/packages/types/pgpm.plan b/packages/types/pgpm.plan index c9653e3f..0baba9f9 100644 --- a/packages/types/pgpm.plan +++ b/packages/types/pgpm.plan @@ -10,3 +10,5 @@ schemas/public/domains/image [schemas/public/schema] 2017-08-11T08:11:51Z skitch schemas/public/domains/origin [schemas/public/schema] 2017-08-11T08:11:51Z skitch # add schemas/public/domains/origin schemas/public/domains/upload [schemas/public/schema] 2017-08-11T08:11:51Z skitch # add schemas/public/domains/upload schemas/public/domains/url [schemas/public/schema] 2017-08-11T08:11:51Z skitch # add schemas/public/domains/url +schemas/public/domains/upload_file_ref_keys [schemas/public/domains/upload] 2026-08-07T00:00:00Z constructive # declare bucket_id, size and filename on upload +schemas/public/domains/image_file_ref_keys [schemas/public/domains/image] 2026-08-07T00:00:00Z constructive # declare bucket_id, size and filename on image diff --git a/packages/types/revert/schemas/public/domains/image_file_ref_keys.sql b/packages/types/revert/schemas/public/domains/image_file_ref_keys.sql new file mode 100644 index 00000000..f58f3cdb --- /dev/null +++ b/packages/types/revert/schemas/public/domains/image_file_ref_keys.sql @@ -0,0 +1,19 @@ +-- Revert schemas/public/domains/image_file_ref_keys from pg + +BEGIN; + +ALTER DOMAIN image DROP CONSTRAINT image_check; + +ALTER DOMAIN image ADD CONSTRAINT image_check CHECK ( + jsonb_typeof(value) = 'object' + AND (value ? 'url' OR value ? 'id' OR value ? 'key') + AND (NOT value ? 'url' OR (value->>'url') ~ '^https?://[^\s]+$') + AND (NOT value ? 'id' OR jsonb_typeof(value->'id') = 'string') + AND (NOT value ? 'key' OR jsonb_typeof(value->'key') = 'string') + AND (NOT value ? 'bucket' OR jsonb_typeof(value->'bucket') = 'string') + AND (NOT value ? 'provider' OR jsonb_typeof(value->'provider') = 'string') + AND (NOT value ? 'mime' OR jsonb_typeof(value->'mime') = 'string') + AND (NOT value ? 'versions' OR jsonb_typeof(value->'versions') = 'array') +); + +COMMIT; diff --git a/packages/types/revert/schemas/public/domains/upload_file_ref_keys.sql b/packages/types/revert/schemas/public/domains/upload_file_ref_keys.sql new file mode 100644 index 00000000..ab11e2d4 --- /dev/null +++ b/packages/types/revert/schemas/public/domains/upload_file_ref_keys.sql @@ -0,0 +1,18 @@ +-- Revert schemas/public/domains/upload_file_ref_keys from pg + +BEGIN; + +ALTER DOMAIN upload DROP CONSTRAINT upload_check; + +ALTER DOMAIN upload ADD CONSTRAINT upload_check CHECK ( + jsonb_typeof(value) = 'object' + AND (value ? 'url' OR value ? 'id' OR value ? 'key') + AND (NOT value ? 'url' OR (value->>'url') ~ '^https?://[^\s]+$') + AND (NOT value ? 'id' OR jsonb_typeof(value->'id') = 'string') + AND (NOT value ? 'key' OR jsonb_typeof(value->'key') = 'string') + AND (NOT value ? 'bucket' OR jsonb_typeof(value->'bucket') = 'string') + AND (NOT value ? 'provider' OR jsonb_typeof(value->'provider') = 'string') + AND (NOT value ? 'mime' OR jsonb_typeof(value->'mime') = 'string') +); + +COMMIT; diff --git a/packages/types/sql/pgpm-types--0.39.0.bundle.tar.gz b/packages/types/sql/pgpm-types--0.39.0.bundle.tar.gz index aa5ddf2c677d2bd8945b27382926e24dc4a3e78c..10dfafeddee94c7bbde72453fdbb3d6b1280549c 100644 GIT binary patch literal 4488 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 a{IlEV_PKp-pWEk`KmQ9Wgvtc~XaE2XdDJ2R literal 3335 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 'url') ~ E'^https?://[^\\s]+$') + AND (NOT (value ? 'id') + OR jsonb_typeof(value -> 'id') = 'string') + AND (NOT (value ? 'key') + OR jsonb_typeof(value -> 'key') = 'string') + AND (NOT (value ? 'bucket') + OR jsonb_typeof(value -> 'bucket') = 'string') + AND (NOT (value ? 'bucket_id') + OR (value ->> 'bucket_id') ~ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') + AND (NOT (value ? 'provider') + OR jsonb_typeof(value -> 'provider') = 'string') + AND (NOT (value ? 'mime') + OR jsonb_typeof(value -> 'mime') = 'string') + AND (NOT (value ? 'size') + OR jsonb_typeof(value -> 'size') = 'number') + AND (NOT (value ? 'filename') + OR jsonb_typeof(value -> 'filename') = 'string') +); + +ALTER DOMAIN image DROP CONSTRAINT image_check; + +ALTER DOMAIN image ADD CONSTRAINT image_check + CHECK ( + jsonb_typeof(value) = 'object' + AND (value ? 'url' + OR value ? 'id' + OR value ? 'key') + AND (NOT (value ? 'url') + OR (value ->> 'url') ~ E'^https?://[^\\s]+$') + AND (NOT (value ? 'id') + OR jsonb_typeof(value -> 'id') = 'string') + AND (NOT (value ? 'key') + OR jsonb_typeof(value -> 'key') = 'string') + AND (NOT (value ? 'bucket') + OR jsonb_typeof(value -> 'bucket') = 'string') + AND (NOT (value ? 'bucket_id') + OR (value ->> 'bucket_id') ~ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') + AND (NOT (value ? 'provider') + OR jsonb_typeof(value -> 'provider') = 'string') + AND (NOT (value ? 'mime') + OR jsonb_typeof(value -> 'mime') = 'string') + AND (NOT (value ? 'size') + OR jsonb_typeof(value -> 'size') = 'number') + AND (NOT (value ? 'filename') + OR jsonb_typeof(value -> 'filename') = 'string') + AND (NOT (value ? 'versions') + OR jsonb_typeof(value -> 'versions') = 'array') +); \ No newline at end of file diff --git a/packages/types/verify/schemas/public/domains/image_file_ref_keys.sql b/packages/types/verify/schemas/public/domains/image_file_ref_keys.sql new file mode 100644 index 00000000..6381eca9 --- /dev/null +++ b/packages/types/verify/schemas/public/domains/image_file_ref_keys.sql @@ -0,0 +1,29 @@ +-- Verify schemas/public/domains/image_file_ref_keys on pg + +BEGIN; + +SELECT assert_domain('public.image'::regtype, 'jsonb'::regtype, _constraints => 1); + +DO $$ +BEGIN + PERFORM '{"id": "0d1e3d64-1e2a-4c7f-9c3a-6f7f9f2b1c44", "key": "abc", "bucket_id": "9a7f1c2e-4b6d-4a11-8f30-2c5d7e9a0b13", "size": 12345, "filename": "hero.png"}'::jsonb::public.image; + + BEGIN + PERFORM '{"id": "abc", "bucket_id": "not-a-uuid"}'::jsonb::public.image; + RAISE EXCEPTION 'public.image accepted a bucket_id that is not a uuid'; + EXCEPTION WHEN check_violation THEN + NULL; + END; + + BEGIN + PERFORM '{"id": "abc", "size": "12345"}'::jsonb::public.image; + RAISE EXCEPTION 'public.image accepted a size that is not a number'; + EXCEPTION WHEN check_violation THEN + NULL; + END; + + PERFORM '{"url": "https://example.com/hero.png", "mime": "image/png"}'::jsonb::public.image; +END +$$; + +ROLLBACK; diff --git a/packages/types/verify/schemas/public/domains/upload_file_ref_keys.sql b/packages/types/verify/schemas/public/domains/upload_file_ref_keys.sql new file mode 100644 index 00000000..f3842c3f --- /dev/null +++ b/packages/types/verify/schemas/public/domains/upload_file_ref_keys.sql @@ -0,0 +1,29 @@ +-- Verify schemas/public/domains/upload_file_ref_keys on pg + +BEGIN; + +SELECT assert_domain('public.upload'::regtype, 'jsonb'::regtype, _constraints => 1); + +DO $$ +BEGIN + PERFORM '{"id": "0d1e3d64-1e2a-4c7f-9c3a-6f7f9f2b1c44", "key": "abc", "bucket_id": "9a7f1c2e-4b6d-4a11-8f30-2c5d7e9a0b13", "size": 12345, "filename": "hero.png"}'::jsonb::public.upload; + + BEGIN + PERFORM '{"id": "abc", "bucket_id": "not-a-uuid"}'::jsonb::public.upload; + RAISE EXCEPTION 'public.upload accepted a bucket_id that is not a uuid'; + EXCEPTION WHEN check_violation THEN + NULL; + END; + + BEGIN + PERFORM '{"id": "abc", "size": "12345"}'::jsonb::public.upload; + RAISE EXCEPTION 'public.upload accepted a size that is not a number'; + EXCEPTION WHEN check_violation THEN + NULL; + END; + + PERFORM '{"url": "https://example.com/hero.png", "mime": "image/png"}'::jsonb::public.upload; +END +$$; + +ROLLBACK;