Skip to content

Commit

Permalink
feat(javascript): add worker build (#4249)
Browse files Browse the repository at this point in the history
Co-authored-by: Torbjørn Holtmon <torbjornholtmon@gmail.com>
  • Loading branch information
shortcuts and TorbjornHoltmon authored Dec 16, 2024
1 parent 2be65a3 commit d6f48a4
Show file tree
Hide file tree
Showing 13 changed files with 226 additions and 47 deletions.
2 changes: 2 additions & 0 deletions clients/algoliasearch-client-javascript/base.tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type PKG = {

const requesters = {
fetch: '@algolia/requester-fetch',
worker: '@algolia/requester-fetch',
http: '@algolia/requester-node-http',
xhr: '@algolia/requester-browser-xhr',
};
Expand Down Expand Up @@ -36,6 +37,7 @@ export function getDependencies(pkg: PKG, requester: Requester): string[] {
case 'xhr':
return deps.filter((dep) => dep !== requesters.fetch && dep !== requesters.http);
case 'fetch':
case 'worker':
return deps.filter((dep) => dep !== requesters.xhr && dep !== requesters.http);
default:
throw new Error('unknown requester', requester);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { expect, test, vi } from 'vitest';

import { LogLevelEnum } from '../../client-common/src/types';
import { createConsoleLogger } from '../../logger-console/src/logger';
import { algoliasearch as node_algoliasearch } from '../builds/node';
import { algoliasearch, apiClientVersion } from '../builds/worker';

test('sets the ua', () => {
const client = algoliasearch('APP_ID', 'API_KEY');
expect(client.transporter.algoliaAgent).toEqual({
add: expect.any(Function),
value: expect.stringContaining(`Algolia for JavaScript (${apiClientVersion}); Search (${apiClientVersion}); Worker`),
});
});

test('forwards node search helpers', () => {
const client = algoliasearch('APP_ID', 'API_KEY');
expect(client.generateSecuredApiKey).not.toBeUndefined();
expect(client.getSecuredApiKeyRemainingValidity).not.toBeUndefined();
expect(async () => {
const resp = await client.generateSecuredApiKey({ parentApiKey: 'foo', restrictions: { validUntil: 200 } });
client.getSecuredApiKeyRemainingValidity({ securedApiKey: resp });
}).not.toThrow();
});

test('web crypto implementation gives the same result as node crypto', async () => {
const client = algoliasearch('APP_ID', 'API_KEY');
const nodeClient = node_algoliasearch('APP_ID', 'API_KEY');
const resp = await client.generateSecuredApiKey({ parentApiKey: 'foo-bar', restrictions: { validUntil: 200 } });
const nodeResp = await nodeClient.generateSecuredApiKey({
parentApiKey: 'foo-bar',
restrictions: { validUntil: 200 },
});

expect(resp).toEqual(nodeResp);
});

test('with logger', () => {
vi.spyOn(console, 'debug');
vi.spyOn(console, 'info');
vi.spyOn(console, 'error');

const client = algoliasearch('APP_ID', 'API_KEY', {
logger: createConsoleLogger(LogLevelEnum.Debug),
});

expect(async () => {
await client.setSettings({ indexName: 'foo', indexSettings: {} });
expect(console.debug).toHaveBeenCalledTimes(1);
expect(console.info).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledTimes(1);
}).not.toThrow();
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,19 @@ export default defineWorkspace([
},
test: {
include: ['__tests__/algoliasearch.fetch.test.ts'],
name: 'miniflare',
name: 'miniflare fetch',
environment: 'miniflare',
},
},
{
resolve: {
alias: {
'@algolia/client-search': '../../client-search/builds/worker',
},
},
test: {
include: ['__tests__/algoliasearch.worker.test.ts'],
name: 'miniflare worker',
environment: 'miniflare',
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public void processOpts() {
supportingFiles.add(new SupportingFile("client/builds/browser.mustache", "builds", "browser.ts"));
supportingFiles.add(new SupportingFile("client/builds/node.mustache", "builds", "node.ts"));
supportingFiles.add(new SupportingFile("client/builds/fetch.mustache", "builds", "fetch.ts"));
supportingFiles.add(new SupportingFile("client/builds/worker.mustache", "builds", "worker.ts"));
}
// `algoliasearch` related files
else {
Expand All @@ -86,6 +87,7 @@ public void processOpts() {
supportingFiles.add(new SupportingFile("algoliasearch/builds/definition.mustache", "builds", "browser.ts"));
supportingFiles.add(new SupportingFile("algoliasearch/builds/definition.mustache", "builds", "node.ts"));
supportingFiles.add(new SupportingFile("algoliasearch/builds/definition.mustache", "builds", "fetch.ts"));
supportingFiles.add(new SupportingFile("algoliasearch/builds/definition.mustache", "builds", "worker.ts"));
supportingFiles.add(new SupportingFile("algoliasearch/builds/models.mustache", "builds", "models.ts"));

// `lite` builds
Expand Down Expand Up @@ -160,7 +162,7 @@ private void setDefaultGeneratorOptions() {
additionalProperties.put("packageVersion", Helpers.getPackageJsonVersion(packageName));
additionalProperties.put("packageName", packageName);
additionalProperties.put("npmPackageName", isAlgoliasearchClient ? packageName : "@algolia/" + packageName);
additionalProperties.put("nodeSearchHelpers", CLIENT.equals("search") || isAlgoliasearchClient);
additionalProperties.put("searchHelpers", CLIENT.equals("search"));

if (isAlgoliasearchClient) {
var dependencies = new ArrayList<Map<String, Object>>();
Expand Down
29 changes: 1 addition & 28 deletions templates/javascript/clients/client/api/nodeHelpers.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -32,32 +32,5 @@ generateSecuredApiKey: ({
);

const queryParameters = serializeQueryParameters(mergedRestrictions);
return Buffer.from(
createHmac('sha256', parentApiKey)
.update(queryParameters)
.digest('hex') + queryParameters
).toString('base64');
},

/**
* Helper: Retrieves the remaining validity of the previous generated `securedApiKey`, the `ValidUntil` parameter must have been provided.
*
* @summary Helper: Retrieves the remaining validity of the previous generated `secured_api_key`, the `ValidUntil` parameter must have been provided.
* @param getSecuredApiKeyRemainingValidity - The `getSecuredApiKeyRemainingValidity` object.
* @param getSecuredApiKeyRemainingValidity.securedApiKey - The secured API key generated with the `generateSecuredApiKey` method.
*/
getSecuredApiKeyRemainingValidity: ({
securedApiKey,
}: GetSecuredApiKeyRemainingValidityOptions): number => {
const decodedString = Buffer.from(securedApiKey, 'base64').toString(
'ascii'
);
const regex = /validUntil=(\d+)/;
const match = decodedString.match(regex);
if (match === null) {
throw new Error('validUntil not found in given secured api key.');
}

return parseInt(match[1], 10) - Math.round(new Date().getTime() / 1000);
return Buffer.from(createHmac('sha256', parentApiKey).update(queryParameters).digest('hex') + queryParameters,).toString('base64');
},
20 changes: 20 additions & 0 deletions templates/javascript/clients/client/api/searchHelpers.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Helper: Retrieves the remaining validity of the previous generated `securedApiKey`, the `ValidUntil` parameter must have been provided.
*
* @summary Helper: Retrieves the remaining validity of the previous generated `secured_api_key`, the `ValidUntil` parameter must have been provided.
* @param getSecuredApiKeyRemainingValidity - The `getSecuredApiKeyRemainingValidity` object.
* @param getSecuredApiKeyRemainingValidity.securedApiKey - The secured API key generated with the `generateSecuredApiKey` method.
*/
getSecuredApiKeyRemainingValidity: ({
securedApiKey,
}: GetSecuredApiKeyRemainingValidityOptions): number => {
const decodedString = atob(securedApiKey);
const regex = /validUntil=(\d+)/;
const match = decodedString.match(regex);
if (match === null) {
throw new Error('validUntil not found in given secured api key.');
}

return parseInt(match[1], 10) - Math.round(new Date().getTime() / 1000);
},
36 changes: 36 additions & 0 deletions templates/javascript/clients/client/api/workerHelpers.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Helper: Generates a secured API key based on the given `parentApiKey` and given `restrictions`.
*
* @summary Helper: Generates a secured API key based on the given `parentApiKey` and given `restrictions`.
* @param generateSecuredApiKey - The `generateSecuredApiKey` object.
* @param generateSecuredApiKey.parentApiKey - The base API key from which to generate the new secured one.
* @param generateSecuredApiKey.restrictions - A set of properties defining the restrictions of the secured API key.
*/
generateSecuredApiKey: async ({
parentApiKey,
restrictions = {},
}: GenerateSecuredApiKeyOptions): Promise<string> => {
let mergedRestrictions = restrictions;
if (restrictions.searchParams) {
// merge searchParams with the root restrictions
mergedRestrictions = {
...restrictions,
...restrictions.searchParams,
};

delete mergedRestrictions.searchParams;
}

mergedRestrictions = Object.keys(mergedRestrictions)
.sort()
.reduce(
(acc, key) => {
acc[key] = (mergedRestrictions as any)[key];
return acc;
},
{} as Record<string, unknown>
);

const queryParameters = serializeQueryParameters(mergedRestrictions);
return await generateBase64Hmac(parentApiKey, queryParameters);
},
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ export * from '../model';
import type { GenerateSecuredApiKeyOptions, GetSecuredApiKeyRemainingValidityOptions, SearchClientNodeHelpers } from '../model';
{{/isSearchClient}}

{{#nodeSearchHelpers}}
import {createHmac} from 'node:crypto';
{{/nodeSearchHelpers}}

export function {{clientName}}(
appId: string,
apiKey: string,{{#hasRegionalHost}}region{{#fallbackToAliasHost}}?{{/fallbackToAliasHost}}: Region,{{/hasRegionalHost}}
Expand Down
15 changes: 10 additions & 5 deletions templates/javascript/clients/client/builds/fetch.mustache
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// {{{generationBanner}}}

export type {{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}} = ReturnType<typeof create{{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}}>{{#nodeSearchHelpers}} & SearchClientNodeHelpers{{/nodeSearchHelpers}};
export type {{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}} = ReturnType<typeof create{{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}}>{{#searchHelpers}} & SearchClientNodeHelpers{{/searchHelpers}};

{{#searchHelpers}}
import { createHmac } from 'node:crypto';
{{/searchHelpers}}

{{> client/builds/definition}}
return {
Expand All @@ -13,15 +17,16 @@ export type {{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}} = ReturnTyp
write: {{x-timeouts.server.write}},
},
logger: createNullLogger(),
algoliaAgents: [{ segment: 'Fetch' }],
requester: createFetchRequester(),
algoliaAgents: [{ segment: 'Fetch' }],
responsesCache: createNullCache(),
requestsCache: createNullCache(),
hostsCache: createMemoryCache(),
...options,
}),
{{#nodeSearchHelpers}}
{{#searchHelpers}}
{{> client/api/nodeHelpers}}
{{/nodeSearchHelpers}}
{{> client/api/searchHelpers}}
{{/searchHelpers}}
}
}
}
13 changes: 9 additions & 4 deletions templates/javascript/clients/client/builds/node.mustache
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// {{{generationBanner}}}

export type {{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}} = ReturnType<typeof create{{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}}>{{#nodeSearchHelpers}} & SearchClientNodeHelpers{{/nodeSearchHelpers}};
export type {{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}} = ReturnType<typeof create{{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}}>{{#searchHelpers}} & SearchClientNodeHelpers{{/searchHelpers}};

{{#searchHelpers}}
import { createHmac } from 'node:crypto';
{{/searchHelpers}}

{{> client/builds/definition}}
return {
Expand All @@ -20,8 +24,9 @@ export type {{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}} = ReturnTyp
hostsCache: createMemoryCache(),
...options,
}),
{{#nodeSearchHelpers}}
{{#searchHelpers}}
{{> client/api/nodeHelpers}}
{{/nodeSearchHelpers}}
{{> client/api/searchHelpers}}
{{/searchHelpers}}
}
}
}
58 changes: 58 additions & 0 deletions templates/javascript/clients/client/builds/worker.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// {{{generationBanner}}}

{{#searchHelpers}}
export type SearchClientWorkerHelpers = {
generateSecuredApiKey: (opts: GenerateSecuredApiKeyOptions) => Promise<string>;
getSecuredApiKeyRemainingValidity: (opts: GetSecuredApiKeyRemainingValidityOptions) => number;
}
{{/searchHelpers}}

export type {{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}} = ReturnType<typeof create{{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}}>{{#searchHelpers}} & SearchClientWorkerHelpers{{/searchHelpers}};

{{> client/builds/definition}}
return {
...create{{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}}({
appId,
apiKey,{{#hasRegionalHost}}region,{{/hasRegionalHost}}
timeouts: {
connect: {{x-timeouts.server.connect}},
read: {{x-timeouts.server.read}},
write: {{x-timeouts.server.write}},
},
logger: createNullLogger(),
requester: createFetchRequester(),
algoliaAgents: [{ segment: 'Worker' }],
responsesCache: createNullCache(),
requestsCache: createNullCache(),
hostsCache: createMemoryCache(),
...options,
}),
{{#searchHelpers}}
{{> client/api/workerHelpers}}
{{> client/api/searchHelpers}}
{{/searchHelpers}}
}
}

{{#searchHelpers}}
async function getCryptoKey(secret: string): Promise<CryptoKey> {
const secretBuf = new TextEncoder().encode(secret);
return await crypto.subtle.importKey('raw', secretBuf, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
}

async function generateHmacHex(cryptoKey: CryptoKey, queryParameters: string): Promise<string> {
const encoder = new TextEncoder();
const queryParametersUint8Array = encoder.encode(queryParameters);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, queryParametersUint8Array);
return Array.from(new Uint8Array(signature))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}

async function generateBase64Hmac(parentApiKey: string, queryParameters: string): Promise<string> {
const crypotKey = await getCryptoKey(parentApiKey);
const hmacHex = await generateHmacHex(crypotKey, queryParameters);
const combined = hmacHex + queryParameters;
return btoa(combined);
}
{{/searchHelpers}}
8 changes: 4 additions & 4 deletions templates/javascript/clients/package.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"require": "./dist/builds/node.cjs"
},
"worker": {
"types": "./dist/fetch.d.ts",
"default": "./dist/builds/fetch.js"
"types": "./dist/worker.d.ts",
"default": "./dist/builds/worker.js"
},
"default": {
"types": "./dist/browser.d.ts",
Expand Down Expand Up @@ -75,8 +75,8 @@
"require": "./dist/node.cjs"
},
"worker": {
"types": "./dist/fetch.d.ts",
"default": "./dist/fetch.js"
"types": "./dist/worker.d.ts",
"default": "./dist/worker.js"
},
"default": {
"types": "./dist/browser.d.ts",
Expand Down
17 changes: 17 additions & 0 deletions templates/javascript/clients/tsup.config.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ const nodeConfigs: Options[] = [
external: getDependencies(pkg, 'fetch'),
entry: ['builds/fetch.ts', 'src/*.ts'],
},
{
...nodeOptions,
format: 'esm',
name: `worker ${pkg.name} esm`,
dts: { entry: { 'worker': 'builds/worker.ts' } },
external: getDependencies(pkg, 'worker'),
entry: ['builds/worker.ts', 'src/*.ts'],
},
{{/isAlgoliasearchClient}}
{{#isAlgoliasearchClient}}
{
Expand Down Expand Up @@ -61,6 +69,15 @@ const nodeConfigs: Options[] = [
outDir: 'dist',
external: getDependencies(pkg, 'fetch'),
},
{
...nodeOptions,
format: 'esm',
name: 'worker algoliasearch esm',
dts: { entry: { 'worker': 'builds/worker.ts' } },
entry: ['builds/worker.ts'],
outDir: 'dist',
external: getDependencies(pkg, 'worker'),
},
{{/isAlgoliasearchClient}}
];

Expand Down

0 comments on commit d6f48a4

Please sign in to comment.