Skip to content

Commit d6f48a4

Browse files
shortcutsTorbjornHoltmon
andauthoredDec 16, 2024··
feat(javascript): add worker build (#4249)
Co-authored-by: Torbjørn Holtmon <torbjornholtmon@gmail.com>
1 parent 2be65a3 commit d6f48a4

File tree

13 files changed

+226
-47
lines changed

13 files changed

+226
-47
lines changed
 

‎clients/algoliasearch-client-javascript/base.tsup.config.ts

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type PKG = {
88

99
const requesters = {
1010
fetch: '@algolia/requester-fetch',
11+
worker: '@algolia/requester-fetch',
1112
http: '@algolia/requester-node-http',
1213
xhr: '@algolia/requester-browser-xhr',
1314
};
@@ -36,6 +37,7 @@ export function getDependencies(pkg: PKG, requester: Requester): string[] {
3637
case 'xhr':
3738
return deps.filter((dep) => dep !== requesters.fetch && dep !== requesters.http);
3839
case 'fetch':
40+
case 'worker':
3941
return deps.filter((dep) => dep !== requesters.xhr && dep !== requesters.http);
4042
default:
4143
throw new Error('unknown requester', requester);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { expect, test, vi } from 'vitest';
2+
3+
import { LogLevelEnum } from '../../client-common/src/types';
4+
import { createConsoleLogger } from '../../logger-console/src/logger';
5+
import { algoliasearch as node_algoliasearch } from '../builds/node';
6+
import { algoliasearch, apiClientVersion } from '../builds/worker';
7+
8+
test('sets the ua', () => {
9+
const client = algoliasearch('APP_ID', 'API_KEY');
10+
expect(client.transporter.algoliaAgent).toEqual({
11+
add: expect.any(Function),
12+
value: expect.stringContaining(`Algolia for JavaScript (${apiClientVersion}); Search (${apiClientVersion}); Worker`),
13+
});
14+
});
15+
16+
test('forwards node search helpers', () => {
17+
const client = algoliasearch('APP_ID', 'API_KEY');
18+
expect(client.generateSecuredApiKey).not.toBeUndefined();
19+
expect(client.getSecuredApiKeyRemainingValidity).not.toBeUndefined();
20+
expect(async () => {
21+
const resp = await client.generateSecuredApiKey({ parentApiKey: 'foo', restrictions: { validUntil: 200 } });
22+
client.getSecuredApiKeyRemainingValidity({ securedApiKey: resp });
23+
}).not.toThrow();
24+
});
25+
26+
test('web crypto implementation gives the same result as node crypto', async () => {
27+
const client = algoliasearch('APP_ID', 'API_KEY');
28+
const nodeClient = node_algoliasearch('APP_ID', 'API_KEY');
29+
const resp = await client.generateSecuredApiKey({ parentApiKey: 'foo-bar', restrictions: { validUntil: 200 } });
30+
const nodeResp = await nodeClient.generateSecuredApiKey({
31+
parentApiKey: 'foo-bar',
32+
restrictions: { validUntil: 200 },
33+
});
34+
35+
expect(resp).toEqual(nodeResp);
36+
});
37+
38+
test('with logger', () => {
39+
vi.spyOn(console, 'debug');
40+
vi.spyOn(console, 'info');
41+
vi.spyOn(console, 'error');
42+
43+
const client = algoliasearch('APP_ID', 'API_KEY', {
44+
logger: createConsoleLogger(LogLevelEnum.Debug),
45+
});
46+
47+
expect(async () => {
48+
await client.setSettings({ indexName: 'foo', indexSettings: {} });
49+
expect(console.debug).toHaveBeenCalledTimes(1);
50+
expect(console.info).toHaveBeenCalledTimes(1);
51+
expect(console.error).toHaveBeenCalledTimes(1);
52+
}).not.toThrow();
53+
});

‎clients/algoliasearch-client-javascript/packages/algoliasearch/vitest.workspace.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,19 @@ export default defineWorkspace([
3333
},
3434
test: {
3535
include: ['__tests__/algoliasearch.fetch.test.ts'],
36-
name: 'miniflare',
36+
name: 'miniflare fetch',
37+
environment: 'miniflare',
38+
},
39+
},
40+
{
41+
resolve: {
42+
alias: {
43+
'@algolia/client-search': '../../client-search/builds/worker',
44+
},
45+
},
46+
test: {
47+
include: ['__tests__/algoliasearch.worker.test.ts'],
48+
name: 'miniflare worker',
3749
environment: 'miniflare',
3850
},
3951
},

‎generators/src/main/java/com/algolia/codegen/AlgoliaJavascriptGenerator.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ public void processOpts() {
7777
supportingFiles.add(new SupportingFile("client/builds/browser.mustache", "builds", "browser.ts"));
7878
supportingFiles.add(new SupportingFile("client/builds/node.mustache", "builds", "node.ts"));
7979
supportingFiles.add(new SupportingFile("client/builds/fetch.mustache", "builds", "fetch.ts"));
80+
supportingFiles.add(new SupportingFile("client/builds/worker.mustache", "builds", "worker.ts"));
8081
}
8182
// `algoliasearch` related files
8283
else {
@@ -86,6 +87,7 @@ public void processOpts() {
8687
supportingFiles.add(new SupportingFile("algoliasearch/builds/definition.mustache", "builds", "browser.ts"));
8788
supportingFiles.add(new SupportingFile("algoliasearch/builds/definition.mustache", "builds", "node.ts"));
8889
supportingFiles.add(new SupportingFile("algoliasearch/builds/definition.mustache", "builds", "fetch.ts"));
90+
supportingFiles.add(new SupportingFile("algoliasearch/builds/definition.mustache", "builds", "worker.ts"));
8991
supportingFiles.add(new SupportingFile("algoliasearch/builds/models.mustache", "builds", "models.ts"));
9092

9193
// `lite` builds
@@ -160,7 +162,7 @@ private void setDefaultGeneratorOptions() {
160162
additionalProperties.put("packageVersion", Helpers.getPackageJsonVersion(packageName));
161163
additionalProperties.put("packageName", packageName);
162164
additionalProperties.put("npmPackageName", isAlgoliasearchClient ? packageName : "@algolia/" + packageName);
163-
additionalProperties.put("nodeSearchHelpers", CLIENT.equals("search") || isAlgoliasearchClient);
165+
additionalProperties.put("searchHelpers", CLIENT.equals("search"));
164166

165167
if (isAlgoliasearchClient) {
166168
var dependencies = new ArrayList<Map<String, Object>>();

‎templates/javascript/clients/client/api/nodeHelpers.mustache

+1-28
Original file line numberDiff line numberDiff line change
@@ -32,32 +32,5 @@ generateSecuredApiKey: ({
3232
);
3333

3434
const queryParameters = serializeQueryParameters(mergedRestrictions);
35-
return Buffer.from(
36-
createHmac('sha256', parentApiKey)
37-
.update(queryParameters)
38-
.digest('hex') + queryParameters
39-
).toString('base64');
40-
},
41-
42-
/**
43-
* Helper: Retrieves the remaining validity of the previous generated `securedApiKey`, the `ValidUntil` parameter must have been provided.
44-
*
45-
* @summary Helper: Retrieves the remaining validity of the previous generated `secured_api_key`, the `ValidUntil` parameter must have been provided.
46-
* @param getSecuredApiKeyRemainingValidity - The `getSecuredApiKeyRemainingValidity` object.
47-
* @param getSecuredApiKeyRemainingValidity.securedApiKey - The secured API key generated with the `generateSecuredApiKey` method.
48-
*/
49-
getSecuredApiKeyRemainingValidity: ({
50-
securedApiKey,
51-
}: GetSecuredApiKeyRemainingValidityOptions): number => {
52-
const decodedString = Buffer.from(securedApiKey, 'base64').toString(
53-
'ascii'
54-
);
55-
const regex = /validUntil=(\d+)/;
56-
const match = decodedString.match(regex);
57-
58-
if (match === null) {
59-
throw new Error('validUntil not found in given secured api key.');
60-
}
61-
62-
return parseInt(match[1], 10) - Math.round(new Date().getTime() / 1000);
35+
return Buffer.from(createHmac('sha256', parentApiKey).update(queryParameters).digest('hex') + queryParameters,).toString('base64');
6336
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Helper: Retrieves the remaining validity of the previous generated `securedApiKey`, the `ValidUntil` parameter must have been provided.
3+
*
4+
* @summary Helper: Retrieves the remaining validity of the previous generated `secured_api_key`, the `ValidUntil` parameter must have been provided.
5+
* @param getSecuredApiKeyRemainingValidity - The `getSecuredApiKeyRemainingValidity` object.
6+
* @param getSecuredApiKeyRemainingValidity.securedApiKey - The secured API key generated with the `generateSecuredApiKey` method.
7+
*/
8+
getSecuredApiKeyRemainingValidity: ({
9+
securedApiKey,
10+
}: GetSecuredApiKeyRemainingValidityOptions): number => {
11+
const decodedString = atob(securedApiKey);
12+
const regex = /validUntil=(\d+)/;
13+
const match = decodedString.match(regex);
14+
15+
if (match === null) {
16+
throw new Error('validUntil not found in given secured api key.');
17+
}
18+
19+
return parseInt(match[1], 10) - Math.round(new Date().getTime() / 1000);
20+
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Helper: Generates a secured API key based on the given `parentApiKey` and given `restrictions`.
3+
*
4+
* @summary Helper: Generates a secured API key based on the given `parentApiKey` and given `restrictions`.
5+
* @param generateSecuredApiKey - The `generateSecuredApiKey` object.
6+
* @param generateSecuredApiKey.parentApiKey - The base API key from which to generate the new secured one.
7+
* @param generateSecuredApiKey.restrictions - A set of properties defining the restrictions of the secured API key.
8+
*/
9+
generateSecuredApiKey: async ({
10+
parentApiKey,
11+
restrictions = {},
12+
}: GenerateSecuredApiKeyOptions): Promise<string> => {
13+
let mergedRestrictions = restrictions;
14+
if (restrictions.searchParams) {
15+
// merge searchParams with the root restrictions
16+
mergedRestrictions = {
17+
...restrictions,
18+
...restrictions.searchParams,
19+
};
20+
21+
delete mergedRestrictions.searchParams;
22+
}
23+
24+
mergedRestrictions = Object.keys(mergedRestrictions)
25+
.sort()
26+
.reduce(
27+
(acc, key) => {
28+
acc[key] = (mergedRestrictions as any)[key];
29+
return acc;
30+
},
31+
{} as Record<string, unknown>
32+
);
33+
34+
const queryParameters = serializeQueryParameters(mergedRestrictions);
35+
return await generateBase64Hmac(parentApiKey, queryParameters);
36+
},

‎templates/javascript/clients/client/builds/definition.mustache

-4
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ export * from '../model';
3030
import type { GenerateSecuredApiKeyOptions, GetSecuredApiKeyRemainingValidityOptions, SearchClientNodeHelpers } from '../model';
3131
{{/isSearchClient}}
3232

33-
{{#nodeSearchHelpers}}
34-
import {createHmac} from 'node:crypto';
35-
{{/nodeSearchHelpers}}
36-
3733
export function {{clientName}}(
3834
appId: string,
3935
apiKey: string,{{#hasRegionalHost}}region{{#fallbackToAliasHost}}?{{/fallbackToAliasHost}}: Region,{{/hasRegionalHost}}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// {{{generationBanner}}}
22

3-
export type {{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}} = ReturnType<typeof create{{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}}>{{#nodeSearchHelpers}} & SearchClientNodeHelpers{{/nodeSearchHelpers}};
3+
export type {{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}} = ReturnType<typeof create{{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}}>{{#searchHelpers}} & SearchClientNodeHelpers{{/searchHelpers}};
4+
5+
{{#searchHelpers}}
6+
import { createHmac } from 'node:crypto';
7+
{{/searchHelpers}}
48

59
{{> client/builds/definition}}
610
return {
@@ -13,15 +17,16 @@ export type {{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}} = ReturnTyp
1317
write: {{x-timeouts.server.write}},
1418
},
1519
logger: createNullLogger(),
16-
algoliaAgents: [{ segment: 'Fetch' }],
1720
requester: createFetchRequester(),
21+
algoliaAgents: [{ segment: 'Fetch' }],
1822
responsesCache: createNullCache(),
1923
requestsCache: createNullCache(),
2024
hostsCache: createMemoryCache(),
2125
...options,
2226
}),
23-
{{#nodeSearchHelpers}}
27+
{{#searchHelpers}}
2428
{{> client/api/nodeHelpers}}
25-
{{/nodeSearchHelpers}}
29+
{{> client/api/searchHelpers}}
30+
{{/searchHelpers}}
2631
}
27-
}
32+
}

‎templates/javascript/clients/client/builds/node.mustache

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// {{{generationBanner}}}
22

3-
export type {{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}} = ReturnType<typeof create{{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}}>{{#nodeSearchHelpers}} & SearchClientNodeHelpers{{/nodeSearchHelpers}};
3+
export type {{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}} = ReturnType<typeof create{{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}}>{{#searchHelpers}} & SearchClientNodeHelpers{{/searchHelpers}};
4+
5+
{{#searchHelpers}}
6+
import { createHmac } from 'node:crypto';
7+
{{/searchHelpers}}
48

59
{{> client/builds/definition}}
610
return {
@@ -20,8 +24,9 @@ export type {{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}} = ReturnTyp
2024
hostsCache: createMemoryCache(),
2125
...options,
2226
}),
23-
{{#nodeSearchHelpers}}
27+
{{#searchHelpers}}
2428
{{> client/api/nodeHelpers}}
25-
{{/nodeSearchHelpers}}
29+
{{> client/api/searchHelpers}}
30+
{{/searchHelpers}}
2631
}
27-
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// {{{generationBanner}}}
2+
3+
{{#searchHelpers}}
4+
export type SearchClientWorkerHelpers = {
5+
generateSecuredApiKey: (opts: GenerateSecuredApiKeyOptions) => Promise<string>;
6+
getSecuredApiKeyRemainingValidity: (opts: GetSecuredApiKeyRemainingValidityOptions) => number;
7+
}
8+
{{/searchHelpers}}
9+
10+
export type {{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}} = ReturnType<typeof create{{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}}>{{#searchHelpers}} & SearchClientWorkerHelpers{{/searchHelpers}};
11+
12+
{{> client/builds/definition}}
13+
return {
14+
...create{{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}}({
15+
appId,
16+
apiKey,{{#hasRegionalHost}}region,{{/hasRegionalHost}}
17+
timeouts: {
18+
connect: {{x-timeouts.server.connect}},
19+
read: {{x-timeouts.server.read}},
20+
write: {{x-timeouts.server.write}},
21+
},
22+
logger: createNullLogger(),
23+
requester: createFetchRequester(),
24+
algoliaAgents: [{ segment: 'Worker' }],
25+
responsesCache: createNullCache(),
26+
requestsCache: createNullCache(),
27+
hostsCache: createMemoryCache(),
28+
...options,
29+
}),
30+
{{#searchHelpers}}
31+
{{> client/api/workerHelpers}}
32+
{{> client/api/searchHelpers}}
33+
{{/searchHelpers}}
34+
}
35+
}
36+
37+
{{#searchHelpers}}
38+
async function getCryptoKey(secret: string): Promise<CryptoKey> {
39+
const secretBuf = new TextEncoder().encode(secret);
40+
return await crypto.subtle.importKey('raw', secretBuf, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
41+
}
42+
43+
async function generateHmacHex(cryptoKey: CryptoKey, queryParameters: string): Promise<string> {
44+
const encoder = new TextEncoder();
45+
const queryParametersUint8Array = encoder.encode(queryParameters);
46+
const signature = await crypto.subtle.sign('HMAC', cryptoKey, queryParametersUint8Array);
47+
return Array.from(new Uint8Array(signature))
48+
.map((b) => b.toString(16).padStart(2, '0'))
49+
.join('');
50+
}
51+
52+
async function generateBase64Hmac(parentApiKey: string, queryParameters: string): Promise<string> {
53+
const crypotKey = await getCryptoKey(parentApiKey);
54+
const hmacHex = await generateHmacHex(crypotKey, queryParameters);
55+
const combined = hmacHex + queryParameters;
56+
return btoa(combined);
57+
}
58+
{{/searchHelpers}}

‎templates/javascript/clients/package.mustache

+4-4
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
"require": "./dist/builds/node.cjs"
3333
},
3434
"worker": {
35-
"types": "./dist/fetch.d.ts",
36-
"default": "./dist/builds/fetch.js"
35+
"types": "./dist/worker.d.ts",
36+
"default": "./dist/builds/worker.js"
3737
},
3838
"default": {
3939
"types": "./dist/browser.d.ts",
@@ -75,8 +75,8 @@
7575
"require": "./dist/node.cjs"
7676
},
7777
"worker": {
78-
"types": "./dist/fetch.d.ts",
79-
"default": "./dist/fetch.js"
78+
"types": "./dist/worker.d.ts",
79+
"default": "./dist/worker.js"
8080
},
8181
"default": {
8282
"types": "./dist/browser.d.ts",

‎templates/javascript/clients/tsup.config.mustache

+17
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ const nodeConfigs: Options[] = [
3434
external: getDependencies(pkg, 'fetch'),
3535
entry: ['builds/fetch.ts', 'src/*.ts'],
3636
},
37+
{
38+
...nodeOptions,
39+
format: 'esm',
40+
name: `worker ${pkg.name} esm`,
41+
dts: { entry: { 'worker': 'builds/worker.ts' } },
42+
external: getDependencies(pkg, 'worker'),
43+
entry: ['builds/worker.ts', 'src/*.ts'],
44+
},
3745
{{/isAlgoliasearchClient}}
3846
{{#isAlgoliasearchClient}}
3947
{
@@ -61,6 +69,15 @@ const nodeConfigs: Options[] = [
6169
outDir: 'dist',
6270
external: getDependencies(pkg, 'fetch'),
6371
},
72+
{
73+
...nodeOptions,
74+
format: 'esm',
75+
name: 'worker algoliasearch esm',
76+
dts: { entry: { 'worker': 'builds/worker.ts' } },
77+
entry: ['builds/worker.ts'],
78+
outDir: 'dist',
79+
external: getDependencies(pkg, 'worker'),
80+
},
6481
{{/isAlgoliasearchClient}}
6582
];
6683

0 commit comments

Comments
 (0)
Please sign in to comment.