@pgpm/types 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,9 +22,10 @@ Core PostgreSQL data types with SQL scripts.
22
22
 
23
23
  - **email**: Case-insensitive email address validation
24
24
  - **url**: HTTP/HTTPS URL validation
25
+ - **origin**: Origin URL validation (scheme + host)
25
26
  - **hostname**: Domain name validation
26
27
  - **image**: JSON-based image metadata with URL and MIME type
27
- - **attachment**: JSON-based file attachment metadata with URL and MIME type
28
+ - **attachment**: File attachment metadata as a URL string or JSON with URL and MIME type
28
29
  - **upload**: File upload metadata
29
30
  - **single_select**: Single selection field
30
31
  - **multiple_select**: Multiple selection field
@@ -156,7 +157,8 @@ INSERT INTO customers (domain) VALUES
156
157
 
157
158
  ### Image and Attachment Domains
158
159
 
159
- The `image` and `attachment` domains store JSON objects with URL and MIME type information.
160
+ 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.
161
+ The `upload` domain uses the same JSON object shape as `image`, ensuring both the file URL and MIME type are present.
160
162
 
161
163
  ```sql
162
164
  -- Valid image
@@ -166,9 +168,12 @@ INSERT INTO customers (profile_image) VALUES
166
168
  -- Valid attachment
167
169
  INSERT INTO customers (document) VALUES
168
170
  ('{"url": "https://storage.example.com/file.pdf", "mime": "application/pdf"}'::json);
171
+
172
+ -- Valid attachment as plain URL
173
+ INSERT INTO customers (document) VALUES ('https://storage.example.com/favicon.ico');
169
174
  ```
170
175
 
171
- **Structure**: Both domains expect JSON objects with `url` and `mime` properties.
176
+ **Structure**: Image values and JSON-form attachments expect `url` and `mime` properties; attachments also allow a bare URL string.
172
177
 
173
178
  ## Domain Types Reference
174
179
 
@@ -176,10 +181,11 @@ INSERT INTO customers (document) VALUES
176
181
  |--------|-----------|-------------|---------|
177
182
  | `email` | `citext` | Case-insensitive email address | `user@example.com` |
178
183
  | `url` | `text` | HTTP/HTTPS URL | `https://example.com/path` |
184
+ | `origin` | `text` | Origin (scheme + host) | `https://example.com` |
179
185
  | `hostname` | `text` | Domain name without protocol | `example.com` |
180
186
  | `image` | `json` | Image metadata with URL and MIME | `{"url": "...", "mime": "image/jpeg"}` |
181
- | `attachment` | `json` | File attachment metadata | `{"url": "...", "mime": "application/pdf"}` |
182
- | `upload` | `text` | File upload identifier | Various formats |
187
+ | `attachment` | `json` | File attachment URL or metadata | `{"url": "...", "mime": "application/pdf"}` or `https://example.com/favicon.ico` |
188
+ | `upload` | `json` | File upload metadata (URL + MIME) | `{"url": "...", "mime": "application/pdf"}` |
183
189
  | `single_select` | `text` | Single selection value | Text value |
184
190
  | `multiple_select` | `text[]` | Multiple selection values | Array of text values |
185
191
 
@@ -207,7 +213,7 @@ The test suite validates:
207
213
  - Email format validation (valid and invalid cases)
208
214
  - URL format validation with extensive test cases
209
215
  - Hostname format validation
210
- - Image and attachment JSON structure validation
216
+ - Image, upload, and attachment JSON structure validation
211
217
 
212
218
  ## Related Tooling
213
219
 
@@ -44,15 +44,37 @@ const invalidUrls = [
44
44
  ];
45
45
 
46
46
  const validAttachments = [
47
+ 'http://www.foo.bar/some.jpg',
48
+ 'https://foo.bar/some.PNG'
49
+ ];
50
+
51
+ const invalidAttachments = [
52
+ 'hi there',
53
+ 'ftp://foo.bar/some.png',
54
+ 'https:///foo.bar/some.png'
55
+ ];
56
+
57
+ const validImages = [
47
58
  { url: 'http://www.foo.bar/some.jpg', mime: 'image/jpg' },
48
59
  { url: 'https://foo.bar/some.PNG', mime: 'image/jpg' }
49
60
  ];
50
61
 
51
- const invalidAttachments = [
62
+ const invalidImages = [
52
63
  { url: 'hi there' },
53
64
  { url: 'https://foo.bar/some.png' }
54
65
  ];
55
66
 
67
+ const validUploads = [
68
+ { url: 'http://www.foo.bar/some.jpg', mime: 'image/jpg' },
69
+ { url: 'https://foo.bar/some.PNG', mime: 'image/png' }
70
+ ];
71
+
72
+ const invalidUploads = [
73
+ { url: 'hi there' },
74
+ { url: 'https://foo.bar/some.png' },
75
+ { url: 'ftp://foo.bar/some.png', mime: 'image/png' }
76
+ ];
77
+
56
78
  let pg: PgTestClient;
57
79
  let teardown: () => Promise<void>;
58
80
 
@@ -68,7 +90,8 @@ CREATE TABLE customers (
68
90
  image image,
69
91
  attachment attachment,
70
92
  domain hostname,
71
- email email
93
+ email email,
94
+ upload upload
72
95
  );
73
96
  `);
74
97
  });
@@ -87,24 +110,48 @@ afterAll(async () => {
87
110
 
88
111
  describe('types', () => {
89
112
  it('valid attachment and image', async () => {
90
- for (const value of validAttachments) {
91
- await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [value]);
92
- await pg.any(`INSERT INTO customers (attachment) VALUES ($1::json);`, [value]);
113
+ for (const attachment of validAttachments) {
114
+ await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [attachment]);
115
+ }
116
+
117
+ for (const image of validImages) {
118
+ await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]);
93
119
  }
94
120
  });
95
121
 
96
122
  it('invalid attachment and image', async () => {
97
- for (const value of invalidAttachments) {
123
+ for (const attachment of invalidAttachments) {
98
124
  let failed = false;
99
125
  try {
100
- await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [value]);
126
+ await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [attachment]);
101
127
  } catch (e) {
102
128
  failed = true;
103
129
  }
104
130
  expect(failed).toBe(true);
105
- failed = false;
131
+ }
132
+
133
+ for (const image of invalidImages) {
134
+ let failed = false;
135
+ try {
136
+ await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]);
137
+ } catch (e) {
138
+ failed = true;
139
+ }
140
+ expect(failed).toBe(true);
141
+ }
142
+ });
143
+
144
+ it('valid upload', async () => {
145
+ for (const upload of validUploads) {
146
+ await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]);
147
+ }
148
+ });
149
+
150
+ it('invalid upload', async () => {
151
+ for (const upload of invalidUploads) {
152
+ let failed = false;
106
153
  try {
107
- await pg.any(`INSERT INTO customers (image) VALUES ($1);`, [value]);
154
+ await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]);
108
155
  } catch (e) {
109
156
  failed = true;
110
157
  }
@@ -44,15 +44,37 @@ const invalidUrls = [
44
44
  ];
45
45
 
46
46
  const validAttachments = [
47
+ 'http://www.foo.bar/some.jpg',
48
+ 'https://foo.bar/some.PNG'
49
+ ];
50
+
51
+ const invalidAttachments = [
52
+ 'hi there',
53
+ 'ftp://foo.bar/some.png',
54
+ 'https:///foo.bar/some.png'
55
+ ];
56
+
57
+ const validImages = [
47
58
  { url: 'http://www.foo.bar/some.jpg', mime: 'image/jpg' },
48
59
  { url: 'https://foo.bar/some.PNG', mime: 'image/jpg' }
49
60
  ];
50
61
 
51
- const invalidAttachments = [
62
+ const invalidImages = [
52
63
  { url: 'hi there' },
53
64
  { url: 'https://foo.bar/some.png' }
54
65
  ];
55
66
 
67
+ const validUploads = [
68
+ { url: 'http://www.foo.bar/some.jpg', mime: 'image/jpg' },
69
+ { url: 'https://foo.bar/some.PNG', mime: 'image/png' }
70
+ ];
71
+
72
+ const invalidUploads = [
73
+ { url: 'hi there' },
74
+ { url: 'https://foo.bar/some.png' },
75
+ { url: 'ftp://foo.bar/some.png', mime: 'image/png' }
76
+ ];
77
+
56
78
  let pg: PgTestClient;
57
79
  let teardown: () => Promise<void>;
58
80
 
@@ -68,7 +90,8 @@ CREATE TABLE customers (
68
90
  image image,
69
91
  attachment attachment,
70
92
  domain hostname,
71
- email email
93
+ email email,
94
+ upload upload
72
95
  );
73
96
  `);
74
97
  });
@@ -87,24 +110,48 @@ afterAll(async () => {
87
110
 
88
111
  describe('types', () => {
89
112
  it('valid attachment and image', async () => {
90
- for (const value of validAttachments) {
91
- await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [value]);
92
- await pg.any(`INSERT INTO customers (attachment) VALUES ($1::json);`, [value]);
113
+ for (const attachment of validAttachments) {
114
+ await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [attachment]);
115
+ }
116
+
117
+ for (const image of validImages) {
118
+ await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]);
93
119
  }
94
120
  });
95
121
 
96
122
  it('invalid attachment and image', async () => {
97
- for (const value of invalidAttachments) {
123
+ for (const attachment of invalidAttachments) {
98
124
  let failed = false;
99
125
  try {
100
- await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [value]);
126
+ await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [attachment]);
101
127
  } catch (e) {
102
128
  failed = true;
103
129
  }
104
130
  expect(failed).toBe(true);
105
- failed = false;
131
+ }
132
+
133
+ for (const image of invalidImages) {
134
+ let failed = false;
135
+ try {
136
+ await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]);
137
+ } catch (e) {
138
+ failed = true;
139
+ }
140
+ expect(failed).toBe(true);
141
+ }
142
+ });
143
+
144
+ it('valid upload', async () => {
145
+ for (const upload of validUploads) {
146
+ await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]);
147
+ }
148
+ });
149
+
150
+ it('invalid upload', async () => {
151
+ for (const upload of invalidUploads) {
152
+ let failed = false;
106
153
  try {
107
- await pg.any(`INSERT INTO customers (image) VALUES ($1);`, [value]);
154
+ await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]);
108
155
  } catch (e) {
109
156
  failed = true;
110
157
  }
@@ -2,11 +2,6 @@
2
2
  -- requires: schemas/public/schema
3
3
 
4
4
  BEGIN;
5
- CREATE DOMAIN attachment AS jsonb CHECK (
6
- value ?& ARRAY['url', 'mime']
7
- AND
8
- value->>'url' ~ '^(https?)://[^\s/$.?#].[^\s]*$'
9
- );
5
+ CREATE DOMAIN attachment AS text CHECK (VALUE ~ '^(https?)://[^\s/$.?#].[^\s]*$');
10
6
  COMMENT ON DOMAIN attachment IS E'@name launchqlInternalTypeAttachment';
11
7
  COMMIT;
12
-
@@ -1,10 +1,8 @@
1
1
  -- Deploy schemas/public/domains/origin to pg
2
-
3
2
  -- requires: schemas/public/schema
4
3
 
5
4
  BEGIN;
6
-
7
- CREATE DOMAIN origin AS text CHECK (VALUE = substring(VALUE from '^(https?://[^/]*)') );
5
+ CREATE DOMAIN origin AS text CHECK (VALUE = substring(VALUE from '^(https?://[^/]*)'));
8
6
  COMMENT ON DOMAIN origin IS E'@name launchqlInternalTypeOrigin';
9
-
10
7
  COMMIT;
8
+
@@ -4,7 +4,11 @@
4
4
 
5
5
  BEGIN;
6
6
 
7
- CREATE DOMAIN upload AS text CHECK (VALUE ~ '^(https?)://[^\s/$.?#].[^\s]*$');
7
+ CREATE DOMAIN upload AS jsonb CHECK (
8
+ value ?& ARRAY['url', 'mime']
9
+ AND
10
+ value->>'url' ~ '^(https?)://[^\s/$.?#].[^\s]*$'
11
+ );
8
12
  COMMENT ON DOMAIN upload IS E'@name launchqlInternalTypeUpload';
9
13
 
10
14
  COMMIT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pgpm/types",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Core PostgreSQL data types with deploy/verify/revert SQL scripts",
5
5
  "author": "Dan Lynch <pyramation@gmail.com>",
6
6
  "contributors": [
@@ -21,7 +21,7 @@
21
21
  "test:watch": "jest --watch"
22
22
  },
23
23
  "dependencies": {
24
- "@pgpm/verify": "0.12.0"
24
+ "@pgpm/verify": "0.13.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "pgpm": "^0.2.0"
@@ -34,5 +34,5 @@
34
34
  "bugs": {
35
35
  "url": "https://github.com/launchql/pgpm-modules/issues"
36
36
  },
37
- "gitHead": "b106c3afcb5152f177f411bf89bbef392fbd2375"
37
+ "gitHead": "e7de11be3a2fd806929adf48acc556d812c75aa6"
38
38
  }
package/pgpm.plan CHANGED
@@ -8,6 +8,7 @@ schemas/public/domains/email [schemas/public/schema] 2017-08-11T08:11:51Z skitch
8
8
  schemas/public/domains/hostname [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/hostname
9
9
  schemas/public/domains/image [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/image
10
10
  schemas/public/domains/multiple_select [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/multiple_select
11
+ schemas/public/domains/origin [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/origin
11
12
  schemas/public/domains/single_select [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/single_select
12
13
  schemas/public/domains/upload [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/upload
13
14
  schemas/public/domains/url [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/url
@@ -5,3 +5,4 @@ BEGIN;
5
5
  DROP TYPE public.origin;
6
6
 
7
7
  COMMIT;
8
+
@@ -35,12 +35,15 @@ CREATE DOMAIN single_select AS jsonb
35
35
 
36
36
  COMMENT ON DOMAIN single_select IS '@name launchqlInternalTypeSingleSelect';
37
37
 
38
- CREATE DOMAIN upload AS text
39
- CHECK (value ~ E'^(https?)://[^\\s/$.?#].[^\\s]*$');
38
+ CREATE DOMAIN upload AS jsonb
39
+ CHECK (
40
+ value ?& ARRAY['url', 'mime']
41
+ AND (value ->> 'url') ~ E'^(https?)://[^\\s/$.?#].[^\\s]*$'
42
+ );
40
43
 
41
44
  COMMENT ON DOMAIN upload IS '@name launchqlInternalTypeUpload';
42
45
 
43
46
  CREATE DOMAIN url AS text
44
47
  CHECK (value ~ E'^(https?)://[^\\s/$.?#].[^\\s]*$');
45
48
 
46
- COMMENT ON DOMAIN url IS '@name launchqlInternalTypeUrl';
49
+ COMMENT ON DOMAIN url IS '@name launchqlInternalTypeUrl';
@@ -5,3 +5,4 @@ BEGIN;
5
5
  SELECT verify_type ('public.origin');
6
6
 
7
7
  ROLLBACK;
8
+