feat(client): add function alias support (#7)

* feat(client): add function alias support

* fix(client): username in aliased url
This commit is contained in:
Daniel Rochetti 2023-05-02 09:18:25 -07:00 committed by GitHub
parent 0503bbe66a
commit 3b11d468a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 430 additions and 80 deletions

1
.gitignore vendored
View File

@ -40,3 +40,4 @@ Thumbs.db
# Next.js
.next
*.local

View File

@ -1,6 +1,6 @@
# The fal-serverless JS Client
![NPM client](https://img.shields.io/npm/v/@fal-ai/serverless-client?color=purple&label=client)
![NPM client](https://img.shields.io/npm/v/@fal-ai/serverless-client?color=%237527D7&label=client)
![Build](https://img.shields.io/github/actions/workflow/status/fal-ai/serverless-js/build.yml)
![License](https://img.shields.io/github/license/fal-ai/serverless-js)
@ -22,7 +22,7 @@ It also handle platform differences, so it work seamlessly across different JS r
> **Note**
>
> Make sure you followed the [fal-serverless getting started]() so you get your credentials and register your functions.
> Make sure you followed the [fal-serverless getting started](https://docs.fal.ai/fal-serverless/quickstart) so you get your credentials and register your functions.
1. First you need to configure your credentials:

View File

@ -1,57 +1,69 @@
import styles from './index.module.css';
import * as fal from '@fal-ai/serverless-client';
fal.config({
credentials: {
userId: '',
keyId: '',
keySecret: '',
},
});
import { getJoke } from '../services/getJoke';
export async function getServerSideProps(context) {
console.log('About to call a fal serverless function from NodeJS');
const result = await fal.run(
'e300f60b-4a7c-44cd-871d-bea588ef43d6/jokes/add',
{
input: {
joke: 'fal serverless is cool, so the joke is on you!',
try {
const result = await getJoke();
return {
props: {
...result,
},
}
};
} catch (error) {
return {
props: {
error: error.message,
},
};
}
}
function Error(props) {
if (!props.error) {
return null;
}
return (
<div
className="p-4 mb-4 text-sm text-red-800 rounded bg-red-50 dark:bg-gray-800 dark:text-red-400"
role="alert"
>
<span className="font-medium">Error</span> {props.error}
</div>
);
console.log(result);
const random = await fal.run(
'e300f60b-4a7c-44cd-871d-bea588ef43d6/jokes/get',
{
method: 'get',
}
);
console.log(random);
return {
props: {
random,
result,
},
};
}
export function Index(props) {
const handleClick = async (e) => {
e.preventDefault();
try {
const joke = await getJoke();
console.log(joke);
} catch (e) {
console.log(e);
}
};
return (
<div className="container mx-auto p-4">
<h1 className="text-4xl font-bold mb-8">
Hello <code>fal serverless</code>
</h1>
<p className="text-lg">
This page can access <strong>fal serverless</strong> functions when
it&apos;s rendering.
</p>
<p>
Added joke with success?{' '}
<strong>{props.result.success.toString()}</strong>
</p>
<p>
Joke <strong>{props.random.joke}</strong>
</p>
<div className="min-h-screen dark:bg-gray-900 dark:text-white bg-white text-black">
<main className="flex flex-col items-center justify-center w-full flex-1 px-20 py-10">
<h1 className="text-4xl font-bold mb-8">
Hello <code>fal-serverless</code>
</h1>
<p className="text-lg mb-10">
This page can access <strong>fal-serverless</strong> functions when
it&apos;s rendering.
</p>
<Error error={props.error} />
<button
onClick={handleClick}
className="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold text-xl py-4 px-8 mx-auto rounded focus:outline-none focus:shadow-outline"
>
Get Joke
</button>
<p className="mt-10">
Here&apos;s a joke: <strong>{props.joke}</strong>
</p>
</main>
</div>
);
}

View File

@ -18,12 +18,10 @@ fal.config({
export async function generateImage(
input: GenerateImageInput
): Promise<ImageDataUri> {
const result = await fal.run(
'a51c0ca0-9011-4ff0-8dc1-2ac0b42a9fd0/generate',
{
input,
}
);
const result = await fal.run('a51c0ca0-9011-4ff0-8dc1-2ac0b42a9fd0', {
path: '/generate',
input,
});
const data = result['raw_data'];
return `data:image/jpg;base64,${data}`;
}

View File

@ -0,0 +1,18 @@
import * as fal from '@fal-ai/serverless-client';
fal.config({
host: 'gateway.alpha.fal.ai',
credentials: {
userId: process.env.FAL_USER_ID || '',
keyId: process.env.FAL_KEY_ID || '',
keySecret: process.env.FAL_KEY_SECRET || '',
},
});
export type GetJokeInput = {
language?: string;
};
export function getJoke(input?: GetJokeInput): Promise<{ joke: string }> {
return fal.run('fastapi_get_joke', { input });
}

View File

@ -12,7 +12,7 @@ export type Config = {
export type RequiredConfig = Required<Config>;
const DEFAULT_CONFIG: Partial<Config> = {
host: 'https://gateway.shark.fal.ai',
host: 'gateway.shark.fal.ai',
};
let configuration: RequiredConfig | undefined = undefined;
@ -33,7 +33,7 @@ export function config(config: Config) {
*/
export function getConfig(): RequiredConfig {
if (typeof configuration === 'undefined') {
throw new Error('You must configure fal serverless first.');
throw new Error('You must configure fal-serverless first.');
}
return configuration;
}

View File

@ -0,0 +1,28 @@
import { randomUUID } from 'crypto';
import { config, getConfig } from './config';
import { buildUrl } from './function';
config({
host: 'gateway.alpha.fal.ai',
credentials: {
userId: 'github|123456',
keyId: 'a91ff3ca-71bc-4c8c-b400-859f6cbe804d',
keySecret: '0123456789abcdfeghijklmnopqrstuv',
},
});
describe('The function test suite', () => {
it('should build the URL with a function UUIDv4', () => {
const { credentials } = getConfig();
const id = randomUUID();
const url = buildUrl(id);
expect(url).toMatch(`trigger/${credentials.userId}/${id}`);
});
it('should build the URL with a function alias', () => {
const { host } = getConfig();
const alias = 'some-alias';
const url = buildUrl(alias);
expect(url).toMatch(`${alias}.${host}`);
});
});

View File

@ -1,12 +1,22 @@
import fetch from 'cross-fetch';
import { getConfig } from './config';
import { getUserAgent, isBrowser } from './runtime';
import { isUUIDv4 } from './utils';
/**
* The function input and other configuration when running
* the function, such as the HTTP method to use.
*/
type RunOptions<Input> = {
/**
* The path to the function, if any. Defaults to `/`.
*/
readonly path?: string;
/**
* The function input. It will be submitted either as query params
* or the body payload, depending on the `method`.
*/
readonly input?: Input;
/**
@ -15,6 +25,35 @@ type RunOptions<Input> = {
readonly method?: 'get' | 'post' | 'put' | 'delete';
};
/**
* Builds the final url to run the function based on its `id` or alias and
* a the options from `RunOptions<Input>`.
*
* @private
* @param id the function id or alias
* @param options the run options
* @returns the final url to run the function
*/
export function buildUrl<Input>(
id: string,
options: RunOptions<Input> = {}
): string {
const { credentials, host } = getConfig();
const method = (options.method ?? 'post').toLowerCase();
const path = options.path ?? '';
const params =
method === 'get' ? new URLSearchParams(options.input ?? {}) : undefined;
let queryParams = '';
if (params) {
queryParams = `?${params.toString()}`;
}
if (isUUIDv4(id)) {
return `https://${host}/trigger/${credentials.userId}/${id}/${path}${queryParams}`;
}
const userId = credentials.userId.replace(/github\|/g, '');
return `https://${userId}-${id}.${host}/${path}${queryParams}`;
}
/**
* Runs a fal serverless function identified by its `id`.
* TODO: expand documentation and provide examples
@ -24,31 +63,32 @@ type RunOptions<Input> = {
*/
export async function run<Input, Output>(
id: string,
options?: RunOptions<Input>
options: RunOptions<Input> = {}
): Promise<Output> {
const { credentials, host } = getConfig();
const { credentials } = getConfig();
const method = (options.method ?? 'post').toLowerCase();
const params =
method === 'get' ? new URLSearchParams(options.input ?? {}).toString() : '';
const userAgent = isBrowser ? {} : { 'User-Agent': getUserAgent() };
const response = await fetch(
`${host}/trigger/${credentials.userId}/${id}${params}`,
{
method,
headers: {
'X-Fal-Key-Id': credentials.keyId,
'X-Fal-Key-Secret': credentials.keySecret,
Accept: 'application/json',
'Content-Type': 'application/json',
...userAgent,
},
mode: 'cors',
body:
method !== 'get' && options.input
? JSON.stringify(options.input)
: null,
}
);
const response = await fetch(buildUrl(id, options), {
method,
headers: {
'X-Fal-Key-Id': credentials.keyId,
'X-Fal-Key-Secret': credentials.keySecret,
'Content-Type': 'application/json',
...userAgent,
},
mode: 'cors',
body:
method !== 'get' && options.input
? JSON.stringify(options.input)
: undefined,
});
const { status, statusText } = response;
if (status < 200 || status >= 300) {
// TODO better error type so handlers can differentiate
throw new Error(statusText);
}
// TODO move this elsewhere so it can be reused by websocket impl too
const contentType = response.headers.get('Content-Type');
if (contentType?.includes('application/json')) {

View File

@ -1,4 +1,4 @@
export type { Credentials } from './config';
export { config } from './config';
export type { FunctionExecution, ProgressEvent } from './function';
export type { Credentials } from './config';
export { run } from './function';
export type { FunctionExecution, ProgressEvent } from './function';

View File

@ -0,0 +1,14 @@
import { randomUUID } from 'crypto';
import { isUUIDv4 } from './utils';
describe('The utils test suite', () => {
it('should match a valid v4 uuid', () => {
const id = randomUUID();
expect(isUUIDv4(id)).toBe(true);
});
it('should not match invalid v4 id', () => {
const id = 'e726b886-e2c2-11ed-b5ea-0242ac120002';
expect(isUUIDv4(id)).toBe(false);
});
});

8
libs/client/src/utils.ts Normal file
View File

@ -0,0 +1,8 @@
export function isUUIDv4(id: string): boolean {
return (
typeof id === 'string' &&
id.length === 36 &&
id[14] === '4' &&
['8', '9', 'a', 'b'].includes(id[19])
);
}

231
package-lock.json generated
View File

@ -3179,6 +3179,126 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@next/swc-android-arm-eabi": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.1.1.tgz",
"integrity": "sha512-qnFCx1kT3JTWhWve4VkeWuZiyjG0b5T6J2iWuin74lORCupdrNukxkq9Pm+Z7PsatxuwVJMhjUoYz7H4cWzx2A==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-android-arm64": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.1.1.tgz",
"integrity": "sha512-eCiZhTzjySubNqUnNkQCjU3Fh+ep3C6b5DCM5FKzsTH/3Gr/4Y7EiaPZKILbvnXmhWtKPIdcY6Zjx51t4VeTfA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.1.1.tgz",
"integrity": "sha512-9zRJSSIwER5tu9ADDkPw5rIZ+Np44HTXpYMr0rkM656IvssowPxmhK0rTreC1gpUCYwFsRbxarUJnJsTWiutPg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.1.1.tgz",
"integrity": "sha512-qWr9qEn5nrnlhB0rtjSdR00RRZEtxg4EGvicIipqZWEyayPxhUu6NwKiG8wZiYZCLfJ5KWr66PGSNeDMGlNaiA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-freebsd-x64": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.1.1.tgz",
"integrity": "sha512-UwP4w/NcQ7V/VJEj3tGVszgb4pyUCt3lzJfUhjDMUmQbzG9LDvgiZgAGMYH6L21MoyAATJQPDGiAMWAPKsmumA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm-gnueabihf": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.1.1.tgz",
"integrity": "sha512-CnsxmKHco9sosBs1XcvCXP845Db+Wx1G0qouV5+Gr+HT/ZlDYEWKoHVDgnJXLVEQzq4FmHddBNGbXvgqM1Gfkg==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.1.1.tgz",
"integrity": "sha512-JfDq1eri5Dif+VDpTkONRd083780nsMCOKoFG87wA0sa4xL8LGcXIBAkUGIC1uVy9SMsr2scA9CySLD/i+Oqiw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.1.1.tgz",
"integrity": "sha512-GA67ZbDq2AW0CY07zzGt07M5b5Yaq5qUpFIoW3UFfjOPgb0Sqf3DAW7GtFMK1sF4ROHsRDMGQ9rnT0VM2dVfKA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.1.1.tgz",
@ -3209,6 +3329,51 @@
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.1.1.tgz",
"integrity": "sha512-pzUHOGrbgfGgPlOMx9xk3QdPJoRPU+om84hqVoe6u+E0RdwOG0Ho/2UxCgDqmvpUrMab1Deltlt6RqcXFpnigQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.1.1.tgz",
"integrity": "sha512-WeX8kVS46aobM9a7Xr/kEPcrTyiwJqQv/tbw6nhJ4fH9xNZ+cEcyPoQkwPo570dCOLz3Zo9S2q0E6lJ/EAUOBg==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.1.1.tgz",
"integrity": "sha512-mVF0/3/5QAc5EGVnb8ll31nNvf3BWpPY4pBb84tk+BfQglWLqc5AC9q1Ht/YMWiEgs8ALNKEQ3GQnbY0bJF2Gg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -29665,6 +29830,54 @@
}
}
},
"@next/swc-android-arm-eabi": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.1.1.tgz",
"integrity": "sha512-qnFCx1kT3JTWhWve4VkeWuZiyjG0b5T6J2iWuin74lORCupdrNukxkq9Pm+Z7PsatxuwVJMhjUoYz7H4cWzx2A==",
"optional": true
},
"@next/swc-android-arm64": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.1.1.tgz",
"integrity": "sha512-eCiZhTzjySubNqUnNkQCjU3Fh+ep3C6b5DCM5FKzsTH/3Gr/4Y7EiaPZKILbvnXmhWtKPIdcY6Zjx51t4VeTfA==",
"optional": true
},
"@next/swc-darwin-arm64": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.1.1.tgz",
"integrity": "sha512-9zRJSSIwER5tu9ADDkPw5rIZ+Np44HTXpYMr0rkM656IvssowPxmhK0rTreC1gpUCYwFsRbxarUJnJsTWiutPg==",
"optional": true
},
"@next/swc-darwin-x64": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.1.1.tgz",
"integrity": "sha512-qWr9qEn5nrnlhB0rtjSdR00RRZEtxg4EGvicIipqZWEyayPxhUu6NwKiG8wZiYZCLfJ5KWr66PGSNeDMGlNaiA==",
"optional": true
},
"@next/swc-freebsd-x64": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.1.1.tgz",
"integrity": "sha512-UwP4w/NcQ7V/VJEj3tGVszgb4pyUCt3lzJfUhjDMUmQbzG9LDvgiZgAGMYH6L21MoyAATJQPDGiAMWAPKsmumA==",
"optional": true
},
"@next/swc-linux-arm-gnueabihf": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.1.1.tgz",
"integrity": "sha512-CnsxmKHco9sosBs1XcvCXP845Db+Wx1G0qouV5+Gr+HT/ZlDYEWKoHVDgnJXLVEQzq4FmHddBNGbXvgqM1Gfkg==",
"optional": true
},
"@next/swc-linux-arm64-gnu": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.1.1.tgz",
"integrity": "sha512-JfDq1eri5Dif+VDpTkONRd083780nsMCOKoFG87wA0sa4xL8LGcXIBAkUGIC1uVy9SMsr2scA9CySLD/i+Oqiw==",
"optional": true
},
"@next/swc-linux-arm64-musl": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.1.1.tgz",
"integrity": "sha512-GA67ZbDq2AW0CY07zzGt07M5b5Yaq5qUpFIoW3UFfjOPgb0Sqf3DAW7GtFMK1sF4ROHsRDMGQ9rnT0VM2dVfKA==",
"optional": true
},
"@next/swc-linux-x64-gnu": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.1.1.tgz",
@ -29677,6 +29890,24 @@
"integrity": "sha512-CM9xnAQNIZ8zf/igbIT/i3xWbQZYaF397H+JroF5VMOCUleElaMdQLL5riJml8wUfPoN3dtfn2s4peSr3azz/g==",
"optional": true
},
"@next/swc-win32-arm64-msvc": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.1.1.tgz",
"integrity": "sha512-pzUHOGrbgfGgPlOMx9xk3QdPJoRPU+om84hqVoe6u+E0RdwOG0Ho/2UxCgDqmvpUrMab1Deltlt6RqcXFpnigQ==",
"optional": true
},
"@next/swc-win32-ia32-msvc": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.1.1.tgz",
"integrity": "sha512-WeX8kVS46aobM9a7Xr/kEPcrTyiwJqQv/tbw6nhJ4fH9xNZ+cEcyPoQkwPo570dCOLz3Zo9S2q0E6lJ/EAUOBg==",
"optional": true
},
"@next/swc-win32-x64-msvc": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.1.1.tgz",
"integrity": "sha512-mVF0/3/5QAc5EGVnb8ll31nNvf3BWpPY4pBb84tk+BfQglWLqc5AC9q1Ht/YMWiEgs8ALNKEQ3GQnbY0bJF2Gg==",
"optional": true
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",