Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions packages/types/__tests__/domains.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 = [
Expand All @@ -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']
];
Expand All @@ -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 = [
Expand All @@ -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']
];
Expand Down Expand Up @@ -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();
});
});
});
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions packages/types/pgpm.plan
Original file line number Diff line number Diff line change
Expand Up @@ -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 <skitch@5b0c196eeb62> # add schemas/public/domains/origin
schemas/public/domains/upload [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/upload
schemas/public/domains/url [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/url
schemas/public/domains/upload_file_ref_keys [schemas/public/domains/upload] 2026-08-07T00:00:00Z constructive <constructive@5b0c196eeb62> # 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 <constructive@5b0c196eeb62> # declare bucket_id, size and filename on image
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Binary file modified packages/types/sql/pgpm-types--0.39.0.bundle.tar.gz
Binary file not shown.
60 changes: 59 additions & 1 deletion packages/types/sql/pgpm-types--0.39.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,62 @@ COMMENT ON DOMAIN upload IS '@name constructiveInternalTypeUpload';
CREATE DOMAIN url AS text
CHECK (value ~ E'^https?://[^\\s]+$');

COMMENT ON DOMAIN url IS '@name constructiveInternalTypeUrl';
COMMENT ON DOMAIN url IS '@name constructiveInternalTypeUrl';

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') ~ 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')
);
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Loading