@balena/sbvr-types
Advanced tools
Comparing version 3.5.0-build-web-resource-2-a2bd417d257b1cfe74d327855816d43274c04b4c-1 to 3.5.0-build-web-resource-2-bcf04a246da11367b67f9683c035a749b5741ca2-1
@@ -8,6 +8,5 @@ # Change Log | ||
# v3.5.0 | ||
## (2023-02-06) | ||
## (2023-03-10) | ||
* Address feedback on new sbvr-type [Nitish Agarwal] | ||
* Add type WebResource [Ramiro González Maciel] | ||
* Adds WebResource type [Otávio Jacobi] | ||
@@ -14,0 +13,0 @@ # v3.4.19 |
@@ -1,4 +0,2 @@ | ||
/// <reference types="node" /> | ||
import { Buffer } from 'buffer'; | ||
export type WebResourceRef = { | ||
export type WebResource = { | ||
filename: string; | ||
@@ -10,10 +8,2 @@ href: string; | ||
}; | ||
export type WebResourceInput = { | ||
filename: string; | ||
data: Buffer; | ||
contentType?: string; | ||
contentDisposition?: string; | ||
size?: number; | ||
storage: string; | ||
}; | ||
export declare const types: { | ||
@@ -30,7 +20,7 @@ postgres: string; | ||
has: { | ||
Filename: (from: any) => any[]; | ||
HRef: (from: any) => any[]; | ||
'Content Type': (from: any) => any[]; | ||
'Content Disposition': (from: any) => any[]; | ||
Size: (from: any) => any[]; | ||
Filename: (stringifiedResource: string) => (string | (string | string[])[])[]; | ||
HRef: (stringifiedResource: string) => (string | (string | string[])[])[]; | ||
'Content Type': (stringifiedResource: string) => (string | (string | string[])[])[]; | ||
'Content Disposition': (stringifiedResource: string) => (string | (string | string[])[])[]; | ||
Size: (stringifiedResource: string) => (string | (string | (string | string[])[])[])[]; | ||
}; | ||
@@ -37,0 +27,0 @@ }; |
@@ -5,4 +5,2 @@ "use strict"; | ||
const TypeUtils = require("../type-utils"); | ||
const index_1 = require("../storage-adapters/index"); | ||
const buffer_1 = require("buffer"); | ||
exports.types = { | ||
@@ -26,26 +24,30 @@ postgres: 'JSONB', | ||
has: { | ||
Filename: (from) => [ | ||
Filename: (stringifiedResource) => [ | ||
'ExtractJSONPathAsText', | ||
from, | ||
stringifiedResource, | ||
['TextArray', ['EmbeddedText', 'filename']], | ||
], | ||
HRef: (from) => [ | ||
HRef: (stringifiedResource) => [ | ||
'ExtractJSONPathAsText', | ||
from, | ||
stringifiedResource, | ||
['TextArray', ['EmbeddedText', 'href']], | ||
], | ||
'Content Type': (from) => [ | ||
'Content Type': (stringifiedResource) => [ | ||
'ExtractJSONPathAsText', | ||
from, | ||
stringifiedResource, | ||
['TextArray', ['EmbeddedText', 'contentType']], | ||
], | ||
'Content Disposition': (from) => [ | ||
'Content Disposition': (stringifiedResource) => [ | ||
'ExtractJSONPathAsText', | ||
from, | ||
stringifiedResource, | ||
['TextArray', ['EmbeddedText', 'contentDisposition']], | ||
], | ||
Size: (from) => [ | ||
'ExtractJSONPathAsText', | ||
from, | ||
['TextArray', ['EmbeddedText', 'size']], | ||
Size: (stringifiedResource) => [ | ||
'Cast', | ||
[ | ||
'ExtractJSONPathAsText', | ||
stringifiedResource, | ||
['TextArray', ['EmbeddedText', 'size']], | ||
], | ||
'Integer', | ||
], | ||
@@ -86,37 +88,26 @@ }, | ||
} | ||
if (!value.filename) { | ||
throw new Error('filename is required'); | ||
} | ||
if (typeof value.filename !== 'string') { | ||
throw new Error('filename string is required'); | ||
throw new Error('filename must be a string'); | ||
} | ||
if (!buffer_1.Buffer.isBuffer(value.data)) { | ||
throw new Error('data is required'); | ||
if (!value.href) { | ||
throw new Error('href is required'); | ||
} | ||
const storage = value.storage; | ||
if (typeof storage !== 'string') { | ||
throw new Error('storage is required'); | ||
if (typeof value.href !== 'string') { | ||
throw new Error('href must be a string'); | ||
} | ||
const storageAdapter = (0, index_1.getStorageAdapter)(storage); | ||
if (!storageAdapter) { | ||
throw new Error(`storage named '${storage}' not defined`); | ||
if (value.contentType && typeof value.contentType !== 'string') { | ||
throw new Error('contentType must be a string'); | ||
} | ||
let webresource; | ||
const contentOptions = { | ||
contentType: value.contentType, | ||
contentDisposition: value.contentDisposition, | ||
}; | ||
try { | ||
webresource = await storageAdapter.saveFile(value.filename, value.data, contentOptions); | ||
if (value.contentDisposition && | ||
typeof value.contentDisposition !== 'string') { | ||
throw new Error('contentDisposition must be a string'); | ||
} | ||
catch (e) { | ||
throw new Error(`can't be saved; error ${e.message}`); | ||
if (value.size && !Number.isInteger(value.size)) { | ||
throw new Error('size must be an integer'); | ||
} | ||
const refData = { | ||
filename: value.filename, | ||
href: webresource.href, | ||
contentType: value.contentType, | ||
contentDisposition: value.contentDisposition, | ||
size: value.size, | ||
}; | ||
try { | ||
const processedValue = JSON.stringify(refData); | ||
return processedValue; | ||
return JSON.stringify(value); | ||
} | ||
@@ -123,0 +114,0 @@ catch (e) { |
{ | ||
"name": "@balena/sbvr-types", | ||
"version": "3.5.0-build-web-resource-2-a2bd417d257b1cfe74d327855816d43274c04b4c-1", | ||
"version": "3.5.0-build-web-resource-2-bcf04a246da11367b67f9683c035a749b5741ca2-1", | ||
"description": "SBVR type definitions.", | ||
@@ -55,4 +55,4 @@ "main": "out", | ||
"versionist": { | ||
"publishedAt": "2023-02-06T13:22:06.975Z" | ||
"publishedAt": "2023-03-10T12:08:27.231Z" | ||
} | ||
} |
@@ -155,33 +155,1 @@ # sbvr-types | ||
Tests can be found under the `test/` folder, to run the whole suite use `npm test` | ||
## Storing files and other large objects | ||
An application can choose between two types to save file content or another large object: `File` or `WebResource`. When using a `File`, PineJS saves the content in the database using a binary data type like `BYTEA` or `BLOB`. When using a `WebResource`, PineJS saves the binary content on an external storage service and then writes metadata, including the content public URL, to the database. Client apps use the `WebResource` `href` to get the content. | ||
### WebResource | ||
Type [`WebResource`](./src//types/web-resource.ts) can be used to persist files or other large content on an external object storage service like MinIO or Amazon S3. By "object storage" we refer to a service that can store the content and provide a URL to access that content. | ||
In order to save a `WebResource` you send an instance of [`WebResourceInput`](./src/types/web-resource.ts#L18) | ||
```js | ||
{ | ||
filename: string; | ||
data: Buffer; | ||
contentType?: string; | ||
contentDisposition?: string; | ||
size?: number; | ||
storage: string; | ||
} | ||
``` | ||
A typical use case is to have a web app where users can upload a file. The app will use [multer](https://github.com/expressjs/multer) to get the file from the http request as a `Buffer` and pass it as the `data` attribute of a `WebResourceInput`. | ||
When retrieving a `WebResource` the `data` attribute is replaced by an `href`( URL ) attribute. See [`WebResourceRef`](./src/types/web-resource.ts#L7) | ||
#### Storage Adapters | ||
The `storage` attribute specifies the name of an [`StorageAdapter`](./src/storage-adapters/storage-adapter.ts#L7). A `StorageAdapter` saves the content to a specific storage, performing a function similar to what a database driver provides. For example, [pinejs-s3-storage](https://github.com/balena-io-modules/pinejs-s3-storage) saves the content to S3/MinIO and returns a URL to the persisted object. For testing purposes, this module uses [disk-storage-adapter](./test/storage-adapters/disk-storage-adapter.js). | ||
Applications need to load the `StorageAdapter`s they need in the `storageRegistry`(./src/storage-adapters/index.ts#L4). Please refer to each specific storage adapter for more configuration and setup details. |
import * as TypeUtils from '../type-utils'; | ||
import { ContentOptions, getStorageAdapter } from '../storage-adapters/index'; | ||
import { Buffer } from 'buffer'; | ||
/** | ||
* This is how WebResources are stored on the DB | ||
*/ | ||
export type WebResourceRef = { | ||
export type WebResource = { | ||
filename: string; | ||
@@ -16,14 +11,2 @@ href: string; | ||
/** | ||
* This is how WebResources are defined on creation | ||
*/ | ||
export type WebResourceInput = { | ||
filename: string; | ||
data: Buffer; | ||
contentType?: string; | ||
contentDisposition?: string; | ||
size?: number; | ||
storage: string; | ||
}; | ||
export const types = { | ||
@@ -48,26 +31,30 @@ postgres: 'JSONB', | ||
has: { | ||
Filename: (from: any) => [ | ||
Filename: (stringifiedResource: string) => [ | ||
'ExtractJSONPathAsText', | ||
from, | ||
stringifiedResource, | ||
['TextArray', ['EmbeddedText', 'filename']], | ||
], | ||
HRef: (from: any) => [ | ||
HRef: (stringifiedResource: string) => [ | ||
'ExtractJSONPathAsText', | ||
from, | ||
stringifiedResource, | ||
['TextArray', ['EmbeddedText', 'href']], | ||
], | ||
'Content Type': (from: any) => [ | ||
'Content Type': (stringifiedResource: string) => [ | ||
'ExtractJSONPathAsText', | ||
from, | ||
stringifiedResource, | ||
['TextArray', ['EmbeddedText', 'contentType']], | ||
], | ||
'Content Disposition': (from: any) => [ | ||
'Content Disposition': (stringifiedResource: string) => [ | ||
'ExtractJSONPathAsText', | ||
from, | ||
stringifiedResource, | ||
['TextArray', ['EmbeddedText', 'contentDisposition']], | ||
], | ||
Size: (from: any) => [ | ||
'ExtractJSONPathAsText', | ||
from, | ||
['TextArray', ['EmbeddedText', 'size']], | ||
Size: (stringifiedResource: string) => [ | ||
'Cast', | ||
[ | ||
'ExtractJSONPathAsText', | ||
stringifiedResource, | ||
['TextArray', ['EmbeddedText', 'size']], | ||
], | ||
'Integer', | ||
], | ||
@@ -77,4 +64,11 @@ }, | ||
/** | ||
* Converts the data, which comes from the DB as a string or object depending on the | ||
* column type, to a WebResource object | ||
* | ||
* @param data string|object | ||
* @returns a WebResource parsed from the DB | ||
*/ | ||
export const fetchProcessing = (data: any) => { | ||
let refData: WebResourceRef; | ||
let refData: WebResource; | ||
if (data === null) { | ||
@@ -108,49 +102,39 @@ return data; | ||
/** | ||
* Validates the value content. | ||
* | ||
* Returns a Stringified WebResource that will be persisted on the DB | ||
* | ||
*/ | ||
export const validate = TypeUtils.validate.checkRequired( | ||
async (value: WebResourceInput) => { | ||
async (value: WebResource) => { | ||
if (typeof value !== 'object') { | ||
throw new Error(`is not an object: ${typeof value}`); | ||
} | ||
if (!value.filename) { | ||
throw new Error('filename is required'); | ||
} | ||
if (typeof value.filename !== 'string') { | ||
throw new Error('filename string is required'); | ||
throw new Error('filename must be a string'); | ||
} | ||
if (!Buffer.isBuffer(value.data)) { | ||
throw new Error('data is required'); | ||
if (!value.href) { | ||
throw new Error('href is required'); | ||
} | ||
const storage = value.storage; | ||
if (typeof storage !== 'string') { | ||
throw new Error('storage is required'); | ||
if (typeof value.href !== 'string') { | ||
throw new Error('href must be a string'); | ||
} | ||
const storageAdapter = getStorageAdapter(storage); | ||
if (!storageAdapter) { | ||
throw new Error(`storage named '${storage}' not defined`); | ||
if (value.contentType && typeof value.contentType !== 'string') { | ||
throw new Error('contentType must be a string'); | ||
} | ||
let webresource; | ||
const contentOptions: ContentOptions = { | ||
contentType: value.contentType, | ||
contentDisposition: value.contentDisposition, | ||
}; | ||
try { | ||
webresource = await storageAdapter.saveFile( | ||
value.filename, | ||
value.data, | ||
contentOptions, | ||
); | ||
} catch (e: any) { | ||
throw new Error(`can't be saved; error ${e.message}`); | ||
if ( | ||
value.contentDisposition && | ||
typeof value.contentDisposition !== 'string' | ||
) { | ||
throw new Error('contentDisposition must be a string'); | ||
} | ||
const refData: WebResourceRef = { | ||
filename: value.filename, | ||
href: webresource.href, | ||
contentType: value.contentType, | ||
contentDisposition: value.contentDisposition, | ||
size: value.size, | ||
}; | ||
if (value.size && !Number.isInteger(value.size)) { | ||
throw new Error('size must be an integer'); | ||
} | ||
try { | ||
const processedValue = JSON.stringify(refData); | ||
return processedValue; | ||
return JSON.stringify(value); | ||
} catch (e: any) { | ||
@@ -157,0 +141,0 @@ throw new Error(`can't be stringified; error ${e.message}`); |
@@ -7,2 +7,3 @@ { | ||
"noImplicitAny": true, | ||
"noUncheckedIndexedAccess": true, | ||
"noUnusedParameters": true, | ||
@@ -9,0 +10,0 @@ "noUnusedLocals": true, |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
121773
97
1921
155