Skip to content

Commit 6c83bf1

Browse files
authored
Merge pull request #36 from invertase/@ehesp/storage-utils-lib
2 parents 0ee0078 + c671dbb commit 6c83bf1

File tree

19 files changed

+1486
-23
lines changed

19 files changed

+1486
-23
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,6 @@ yarn.lock
8686

8787

8888
#vscode
89-
.vscode
89+
.vscode
90+
91+
dist

.prettierignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
node_modules
22
build
3-
lib
3+
**/functions/lib/**/*.js
4+
**/extensions/storage-image-processing-api/lib/src/types/**/*.ts
45
package-lock.json

docs/storage-image-processing-api/index.mdx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,24 @@ const operations = [
129129
const encodedOperations = encodeURIComponent(JSON.stringify(operations));
130130
const url = `https://{LOCATION}-{PROJECT_ID}.cloudfunctions.net/ext-storage-image-processing-api-handler/process?operations=${encodedOperations}`;
131131
```
132+
133+
### JavaScript utility library
134+
135+
Additionally, a utility library exists to provide a fully typed interface for constructing options:
136+
137+
```ts
138+
import { builder } from '@invertase/storage-image-processing-api';
139+
140+
const output = builder()
141+
.input({
142+
source: 'https://example.com/image.jpg',
143+
})
144+
.rotate({ angle: 90 })
145+
.output({
146+
format: 'png',
147+
});
148+
149+
console.log(output.toJSON());
150+
```
151+
152+
For more details, [view the documentation](/utility-library).
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Utilty Library
2+
3+
To assist with the usage of this Extension, a [utility library](https://www.npmjs.com/package/@invertase/storage-image-processing-api) exists
4+
to help provide a typed API interface for easily constructing operations to send to the API.
5+
6+
## Installation
7+
8+
```bash
9+
npm i --save @invertase/storage-image-processing-api
10+
```
11+
12+
## Usage
13+
14+
Once installed, import the `builder` function from the library:
15+
16+
```ts
17+
import { builder } from '@invertase/storage-image-processing-api';
18+
```
19+
20+
The `builder` function returns a new `StorageImageProcessingApi` instance which provides a API for constructing operations to send to the API.
21+
At a minimum, you must provide an `input` and `output` operation as required by the extension itself:
22+
23+
```ts
24+
import { builder } from '@invertase/storage-image-processing-api';
25+
26+
const build = builder()
27+
.input({
28+
url: 'https://example.com/image.jpg',
29+
})
30+
.output({
31+
format: 'png',
32+
});
33+
```
34+
35+
To provide additional operations, you can chain them together. For example,
36+
to apply a blur, grayscale and flip the provided input image, and return a new PNG image:
37+
38+
```ts
39+
const output = builder()
40+
.input({
41+
url: 'https://example.com/image.jpg',
42+
})
43+
.blur()
44+
.grayscale()
45+
.flip()
46+
.output({
47+
format: 'png',
48+
});
49+
```
50+
51+
Please refer to the [operations](/operations) documentation for a full list of available operations, their API and examples.
52+
53+
Once you have constructed your operations, you can return the operations as JSON, a JSON string or encoded JSON string value:
54+
55+
```ts
56+
const json = output.toJSON();
57+
const jsonString = output.toJSONString();
58+
const encodedJsonString = output.toEncodedJSONString();
59+
```
60+
61+
The encoded JSON string value can be passed directly to the extension as the `operations` parameter:
62+
63+
```ts
64+
const encodedJsonString = output.toEncodedJSONString();
65+
66+
const url = `https://{LOCATION}-{PROJECT_ID}.cloudfunctions.net/ext-storage-image-processing-api-handler/process?operations=${encodedJsonString}`;
67+
```

extensions/storage-image-processing-api/functions/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
"scripts": {
55
"prepare": "npm run build",
66
"build": "tsc",
7+
"copy-types": "tsc --emitDeclarationOnly --declaration --allowJs --outDir ../lib/src/types",
78
"build:watch": "tsc --watch",
89
"dev": "EXPRESS_SERVER=true ../../../node_modules/.bin/nodemon",
910
"emulator:local": "FIREBASE_STORAGE_EMULATOR_HOST='localhost:9199' firebase ext:dev:emulators:start --test-config=__tests__/test-firebase.json --test-params=__tests__/test-params.env --project=extensions-testing --import=./__tests__/data",
1011
"generate-readme": "firebase ext:info .. --markdown > ../README.md",
1112
"test:watch": "jest --config=./jest.config.js --watch",
1213
"test": "FIREBASE_STORAGE_EMULATOR_HOST='localhost:9199' firebase ext:dev:emulators:exec ./node_modules/.bin/jest --test-config=__tests__/test-firebase.json --test-params=__tests__/test-params.env --project=extensions-testing --import=./__tests__/data",
13-
"deploy": "firebase ext:install ../ --project=extensions-testing"
14+
"deploy": "firebase ext:install ../ --project=extensions-testing",
15+
"postinstall": "npm run copy-types"
1416
},
1517
"engines": {
1618
"node": "14"

extensions/storage-image-processing-api/functions/src/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
} from './utils';
3636
import { Operation, ValidatedOperation } from './types';
3737
import { extensionConfiguration } from './config';
38+
import sharp from 'sharp';
3839

3940
async function processImageRequest(
4041
validatedOperations: ValidatedOperation[],
@@ -58,14 +59,14 @@ async function processImageRequest(
5859
}
5960

6061
// Apply operations.
61-
let instance = null;
62+
let instance: sharp.Sharp | null = null;
6263
for (let i = 0; i < validatedOperations.length; i++) {
6364
const validatedOperation = validatedOperations[i];
6465
instance = await applyValidatedOperation(instance, validatedOperation);
6566
}
6667

6768
const finalFileMetadata = omitKeys(
68-
await instance.metadata(),
69+
await (instance as sharp.Sharp).metadata(),
6970
fileMetadataBufferKeys,
7071
);
7172

@@ -77,7 +78,9 @@ async function processImageRequest(
7778
return;
7879
}
7980

80-
const output = await instance.toBuffer({ resolveWithObject: true });
81+
const output = await (instance as sharp.Sharp).toBuffer({
82+
resolveWithObject: true,
83+
});
8184
const { data, info } = output;
8285

8386
functions.logger.debug(`Processed a new request.`, validatedOperations);
@@ -125,7 +128,7 @@ app.get(
125128
}),
126129
);
127130
}
128-
let operations: Operation[] = null;
131+
let operations: Operation[] = [];
129132
try {
130133
operations = JSON.parse(decodeURIComponent(operationsString));
131134
} catch (e) {

extensions/storage-image-processing-api/functions/src/operations/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { AssertionError } from 'assert';
18-
import sharp from 'sharp';
18+
import sharp, { SharpOptions } from 'sharp';
1919
import superstruct from 'superstruct';
2020

2121
import { omitKey, omitUndefinedValues } from '../utils';
@@ -174,7 +174,7 @@ export function jsonAsValidatedOperations(
174174
try {
175175
const options = asValidatedOperationOptions(rawOptions);
176176
output.push({
177-
source: null,
177+
source: '',
178178
operation,
179179
rawOptions,
180180
options,
@@ -260,14 +260,14 @@ export async function applyValidatedOperation(
260260
for (let i = 0; i < builtOperation.actions.length; i++) {
261261
const action = builtOperation.actions[i];
262262
if (action.method == 'constructor') {
263-
currentInstance = sharp(...action.arguments);
264-
} else {
263+
currentInstance = sharp(...(action.arguments as SharpOptions[]));
264+
} else if (currentInstance != null) {
265265
currentInstance = (
266266
currentInstance[action.method] as (...args: unknown[]) => sharp.Sharp
267267
)(...action.arguments);
268268
const newBuffer = await currentInstance.toBuffer();
269269
currentInstance = sharp(newBuffer);
270270
}
271271
}
272-
return currentInstance;
272+
return currentInstance as sharp.Sharp;
273273
}

extensions/storage-image-processing-api/functions/src/operations/input.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,16 @@ const struct = superstruct.union([
113113
structCreateNewImage,
114114
]);
115115

116-
export type OperationInput = superstruct.Infer<typeof struct>;
116+
export type OperationInputGcs = superstruct.Infer<typeof structFromGcs>;
117+
export type OperationInputUrl = superstruct.Infer<typeof structFromUrl>;
118+
export type OperationInputCreateNew = superstruct.Infer<
119+
typeof structCreateNewImage
120+
>;
121+
122+
export type OperationInput =
123+
| OperationInputGcs
124+
| OperationInputUrl
125+
| OperationInputCreateNew;
117126

118127
export const operationInput: OperationBuilder = {
119128
name,
@@ -146,7 +155,7 @@ export const operationInput: OperationBuilder = {
146155
};
147156

148157
async function fetchUrl(options: OperationInput): Promise<OperationAction[]> {
149-
if (options.type !== 'url') return;
158+
if (options.type !== 'url') return [];
150159
if (!options.url.startsWith('http')) {
151160
throw new AssertionError({
152161
message: `'${options.url}' does not appear to be a valid http url.`,
@@ -163,7 +172,7 @@ async function fetchUrl(options: OperationInput): Promise<OperationAction[]> {
163172
async function fetchGcsFile(
164173
options: OperationInput,
165174
): Promise<OperationAction[]> {
166-
if (options.type !== 'gcs') return;
175+
if (options.type !== 'gcs') return [];
167176
const firebaseStorageApi =
168177
process.env.NODE_ENV === 'production'
169178
? 'https://firebasestorage.googleapis.com'
@@ -203,7 +212,7 @@ async function fetchGcsFile(
203212
}
204213

205214
function newImageOptions(options: OperationInput): OperationAction[] {
206-
if (options.type !== 'create') return;
215+
if (options.type !== 'create') return [];
207216
const create: Record<string, unknown | Record<string, unknown>> = {};
208217

209218
create.width = options.width;
@@ -221,10 +230,10 @@ function newImageOptions(options: OperationInput): OperationAction[] {
221230
type: 'gaussian',
222231
};
223232
if (options.noiseMean != undefined) {
224-
create.noise['mean'] = options.noiseMean;
233+
(create.noise as Record<string, number>)['mean'] = options.noiseMean;
225234
}
226235
if (options.noiseSigma != undefined) {
227-
create.noise['sigma'] = options.noiseSigma;
236+
(create.noise as Record<string, number>)['sigma'] = options.noiseSigma;
228237
}
229238

230239
return [

extensions/storage-image-processing-api/functions/src/operations/output.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,18 @@ const struct = superstruct.union([
335335
structAvif,
336336
]);
337337

338-
export type OperationOutput = superstruct.Infer<typeof struct>;
338+
export type OperationOutputPng = superstruct.Infer<typeof structPng>;
339+
export type OperationOutputJpeg = superstruct.Infer<typeof structJpeg>;
340+
export type OperationOutputWebp = superstruct.Infer<typeof structWebp>;
341+
export type OperationOutputTiff = superstruct.Infer<typeof structTiff>;
342+
export type OperationOutputAvif = superstruct.Infer<typeof structAvif>;
343+
344+
export type OperationOutput =
345+
| OperationOutputPng
346+
| OperationOutputJpeg
347+
| OperationOutputWebp
348+
| OperationOutputTiff
349+
| OperationOutputAvif;
339350

340351
export const operationOutput: OperationBuilder = {
341352
name,

extensions/storage-image-processing-api/functions/src/utils.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ export function coerceStringToArray<T>(
8080
});
8181
}
8282

83-
export function omitUndefinedValues<T>(object: T): T {
83+
export function omitUndefinedValues<T extends Record<string, unknown>>(
84+
object: T,
85+
): T {
8486
Object.keys(object).forEach(key =>
8587
object[key] === undefined ? delete object[key] : {},
8688
);
@@ -127,12 +129,12 @@ export async function fetchImageBufferFromUrl(url: string): Promise<Buffer> {
127129
responseType: 'arraybuffer',
128130
}),
129131
);
130-
let response: axios.AxiosResponse = possibleResponse;
132+
let response: axios.AxiosResponse | undefined = possibleResponse;
131133
if (possibleError) {
132134
response = possibleError.response;
133135
}
134136

135-
if (response.data && response.status == 200) {
137+
if (response && response.data && response.status == 200) {
136138
const isImage = await isImageFileType(response.data);
137139
if (!isImage) {
138140
throw new AssertionError({
@@ -143,7 +145,7 @@ export async function fetchImageBufferFromUrl(url: string): Promise<Buffer> {
143145
return response.data;
144146
}
145147

146-
const errorMessage = `Unable to fetch image from url "${url}", the url returned a non-successful status code of "${response.status}".`;
148+
const errorMessage = `Unable to fetch image from url "${url}", the url returned a non-successful status code of "${response?.status}".`;
147149
throw new AssertionError({
148150
message: `${errorMessage} The returned error was: ${possibleError.message}`,
149151
});

0 commit comments

Comments
 (0)