@squiz/optimization-utils 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/optimization-utils",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,9 +10,9 @@
10
10
  "build:check": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "@squiz/optimization-http-client": "^1.0.0",
14
- "@squiz/optimization-logger": "^1.0.1",
15
- "@squiz/optimization-value-objects": "^1.0.0",
13
+ "@squiz/optimization-http-client": "^1.0.1",
14
+ "@squiz/optimization-logger": "^1.0.2",
15
+ "@squiz/optimization-value-objects": "^1.0.1",
16
16
  "node-fetch-commonjs": "^3.3.2",
17
17
  "dotenv": "^16.4.5",
18
18
  "@aws-sdk/client-dynamodb": "^3.614.0",
package/CHANGELOG.md DELETED
@@ -1,43 +0,0 @@
1
- # @squiz/optimization-utils
2
-
3
- ## 2.0.2
4
-
5
- ### Patch Changes
6
-
7
- - cf516f7: Cleaned up the optimization/utils
8
-
9
- ## 2.0.1
10
-
11
- ### Patch Changes
12
-
13
- - ca7d105: Removed createLogsLambdaHandler function
14
-
15
- ## 2.0.0
16
-
17
- ### Major Changes
18
-
19
- - abd0e0f: Added EntityId into the DomainEvent interface
20
-
21
- ## 1.3.0
22
-
23
- ### Minor Changes
24
-
25
- - 24aaeac: Added get property function
26
-
27
- ## 1.2.0
28
-
29
- ### Minor Changes
30
-
31
- - 0c347a0: Added remote function into the LogMessage, Updated FilterPattern
32
-
33
- ## 1.1.1
34
-
35
- ### Patch Changes
36
-
37
- - 2a5e0fe: Changed entry points
38
-
39
- ## 1.1.0
40
-
41
- ### Minor Changes
42
-
43
- - afea2f1: Added the logs lambda handler
@@ -1,20 +0,0 @@
1
- import { injectable } from 'inversify';
2
-
3
- export type CloudflareKey = string;
4
-
5
- export type BatchValueData<T> = Array<{
6
- key: CloudflareKey;
7
- value: T;
8
- }>;
9
-
10
- // https://developers.cloudflare.com/api/operations/workers-kv-namespace-write-multiple-key-value-pairs
11
- export const MAX_BATCH_DATA_ITEMS = 10_000;
12
-
13
- @injectable()
14
- export abstract class CloudflareKVHttpService<TSchema> {
15
- abstract getValues(key: CloudflareKey): Promise<TSchema>;
16
-
17
- abstract putBulk(batchData: BatchValueData<TSchema>): Promise<void>;
18
-
19
- abstract deleteBulk(keys: Array<CloudflareKey>): Promise<void>;
20
- }
@@ -1,129 +0,0 @@
1
- import { injectable } from 'inversify';
2
- import { inspect } from 'util';
3
- import {
4
- BatchValueData,
5
- CloudflareKey,
6
- CloudflareKVHttpService,
7
- MAX_BATCH_DATA_ITEMS,
8
- } from './CloudflareKVHttpService';
9
- import { ZodType } from 'zod';
10
- import {
11
- HttpRequestBuilderFactory,
12
- HttpMethod,
13
- } from '@squiz/optimization-http-client';
14
- import { createLog, Logger } from '@squiz/optimization-logger';
15
-
16
- export type CloudflareConfig = {
17
- domain: string;
18
- accountId: string;
19
- namespace: string;
20
- apiKey: string;
21
- };
22
- export type CloudflareConfigProvider = () => Promise<CloudflareConfig>;
23
-
24
- @injectable()
25
- export class ImplCloudflareKVHttpService<TSchema>
26
- implements CloudflareKVHttpService<TSchema>
27
- {
28
- constructor(
29
- private readonly cloudflareConfigProvider: CloudflareConfigProvider,
30
- private readonly httpRequestBuilderFactory: HttpRequestBuilderFactory,
31
- private readonly logger: Logger,
32
- private readonly schema: TSchema extends ZodType ? TSchema : never,
33
- ) {}
34
-
35
- async getValues(key: string): Promise<TSchema> {
36
- const response = await this.callApi({
37
- path: `/values/${key}`,
38
- method: HttpMethod.GET,
39
- handleNotFoundAsEmpty: true,
40
- });
41
-
42
- return this.schema.parse(
43
- typeof response === 'string' ? JSON.parse(response) : {},
44
- );
45
- }
46
-
47
- async putBulk(batchData: BatchValueData<TSchema>): Promise<void> {
48
- if (!batchData.length) {
49
- return;
50
- }
51
-
52
- if (batchData.length > MAX_BATCH_DATA_ITEMS) {
53
- throw new Error(
54
- `The max items count has been exceeded. The max items count is ${MAX_BATCH_DATA_ITEMS}`,
55
- );
56
- }
57
-
58
- await this.callApi({
59
- method: HttpMethod.PUT,
60
- body: batchData.map((data) => ({
61
- ...data,
62
- value: JSON.stringify(data.value),
63
- })),
64
- path: `/bulk`,
65
- });
66
- }
67
-
68
- async deleteBulk(keys: Array<CloudflareKey>): Promise<void> {
69
- if (!keys.length) {
70
- return;
71
- }
72
-
73
- await this.callApi({
74
- method: HttpMethod.DELETE,
75
- body: keys,
76
- path: `/bulk`,
77
- });
78
- }
79
-
80
- private async callApi({
81
- path,
82
- body,
83
- method,
84
- handleNotFoundAsEmpty = false,
85
- }: {
86
- path: '/bulk' | `/values/${string}`;
87
- method: HttpMethod;
88
- body?: Record<string, string> | Array<unknown>;
89
- handleNotFoundAsEmpty?: boolean;
90
- }): Promise<unknown | undefined> {
91
- const config = await this.cloudflareConfigProvider();
92
- const baseUrl =
93
- `https://${config.domain}/client/v4/accounts/` +
94
- `${config.accountId}/storage/kv/namespaces/` +
95
- `${config.namespace}`;
96
- const url = baseUrl + path;
97
-
98
- const logMessage = createLog()
99
- .attachMetadata({ path, method, url })
100
- .create(this);
101
-
102
- this.logger.debug(...logMessage('started calling http endpoint'));
103
- this.logger.debug(
104
- ...logMessage(`the request body: ${inspect(body, { depth: 4 })}`),
105
- );
106
- const httpBuilder = this.httpRequestBuilderFactory
107
- .create()
108
- .url(url)
109
- .method(method)
110
- .body(body)
111
- .applicationJson()
112
- .authorizationByBearer(config.apiKey);
113
-
114
- handleNotFoundAsEmpty && httpBuilder.handleNotFound();
115
-
116
- const response = await httpBuilder.sendRequest();
117
-
118
- this.logger.debug(...logMessage('finished calling http endpoint'));
119
- this.logger.debug(
120
- ...logMessage(
121
- `returned type: ${typeof response.body}, type: returned: ${inspect(
122
- response.body,
123
- { depth: 4 },
124
- )}`,
125
- ),
126
- );
127
- return response.body;
128
- }
129
- }
@@ -1,178 +0,0 @@
1
- import {
2
- CloudflareConfig,
3
- ImplCloudflareKVHttpService,
4
- } from '../ImplCloudflareKVHttpService';
5
- import { Container } from 'inversify';
6
- import {
7
- createHttpClientMock,
8
- HttpRequestBuilderFactory,
9
- HttpClient,
10
- HttpMethod,
11
- HttpRequestOptions,
12
- HttpResponse,
13
- } from '@squiz/optimization-http-client';
14
- import { Logger, createLoggerMock } from '@squiz/optimization-logger';
15
- import { faker } from '@faker-js/faker';
16
- import { z } from 'zod';
17
-
18
- describe('CloudflareKVHttpService', () => {
19
- const EXAMPLE_SCHEMA = z.object({
20
- example: z.string(),
21
- });
22
-
23
- type ExampleSchemaType = z.infer<typeof EXAMPLE_SCHEMA>;
24
-
25
- let service: ImplCloudflareKVHttpService<ExampleSchemaType>;
26
- let httpClient: HttpClient;
27
- const config: CloudflareConfig = {
28
- domain: 'test-domain.com',
29
- accountId: 'test-account-id',
30
- namespace: 'test-namespace-id',
31
- apiKey: 'test-api-key',
32
- };
33
- const expectedHeaders = {
34
- 'Content-Type': 'application/json',
35
- Authorization: `Bearer ${config.apiKey}`,
36
- };
37
-
38
- beforeEach(() => {
39
- const container = new Container({ defaultScope: 'Singleton' });
40
-
41
- container
42
- .bind(ImplCloudflareKVHttpService)
43
- .toDynamicValue(({ container: c }) => {
44
- return new ImplCloudflareKVHttpService(
45
- () => Promise.resolve(config),
46
- c.get(HttpRequestBuilderFactory),
47
- c.get(Logger),
48
- EXAMPLE_SCHEMA,
49
- );
50
- });
51
- container.bind(HttpRequestBuilderFactory).toSelf();
52
- container.bind(HttpClient).toConstantValue(createHttpClientMock());
53
- container.bind(Logger).toConstantValue(createLoggerMock());
54
-
55
- service = container.get(ImplCloudflareKVHttpService);
56
- httpClient = container.get(HttpClient);
57
- });
58
-
59
- describe('getValues', () => {
60
- const key = 'example-key';
61
-
62
- it('should get values from Cloudflare KV', async () => {
63
- const expectedResponse = { example: faker.string.uuid() };
64
-
65
- const expectedURL =
66
- 'https://test-domain.com/client/v4/accounts' +
67
- '/test-account-id/storage/kv/namespaces/test-namespace-id' +
68
- `/values/${key}`;
69
-
70
- jest.spyOn(httpClient, 'sendRequest').mockResolvedValueOnce(
71
- new HttpResponse({
72
- statusCode: 200,
73
- body: JSON.stringify(expectedResponse),
74
- }),
75
- );
76
-
77
- const result = await service.getValues(key);
78
-
79
- expect(result).toEqual(expectedResponse);
80
- expect(httpClient.sendRequest).toHaveBeenCalledWith({
81
- url: expectedURL,
82
- method: HttpMethod.GET,
83
- headers: expectedHeaders,
84
- handleHttpErrorStatusCodes: [404],
85
- } as HttpRequestOptions);
86
- });
87
-
88
- it('should throw an error if fetch fails', async () => {
89
- jest.spyOn(httpClient, 'sendRequest').mockRejectedValueOnce(new Error());
90
-
91
- await expect(service.getValues(key)).rejects.toThrow();
92
- });
93
- });
94
-
95
- describe('putBulk', () => {
96
- it('should call the put bulk endpoint', async () => {
97
- const value: ExampleSchemaType = { example: faker.string.uuid() };
98
- const expectedURL =
99
- 'https://test-domain.com/client/v4/accounts' +
100
- '/test-account-id/storage/kv/namespaces/test-namespace-id' +
101
- `/bulk`;
102
-
103
- jest
104
- .spyOn(httpClient, 'sendRequest')
105
- .mockResolvedValueOnce(
106
- new HttpResponse({ statusCode: 200, body: undefined }),
107
- );
108
-
109
- await service.putBulk([
110
- {
111
- key: 'someKey',
112
- value: value,
113
- },
114
- ]);
115
-
116
- expect(httpClient.sendRequest).toHaveBeenCalledWith({
117
- url: expectedURL,
118
- method: HttpMethod.PUT,
119
- headers: expectedHeaders,
120
- body: [
121
- {
122
- key: 'someKey',
123
- value: JSON.stringify(value),
124
- },
125
- ],
126
- } as HttpRequestOptions);
127
- });
128
-
129
- it('should not call the put bulk endpoint if the batch data array is empty', async () => {
130
- await service.putBulk([]);
131
-
132
- expect(httpClient.sendRequest).toHaveBeenCalledTimes(0);
133
- });
134
-
135
- it('should throw exception if the items amount exceeds 10_000', async () => {
136
- const batch = Array(10_001)
137
- .fill('')
138
- .map(() => ({
139
- key: faker.string.uuid(),
140
- value: { example: faker.string.uuid() },
141
- }));
142
-
143
- await expect(() => service.putBulk(batch)).rejects.toThrow(
144
- 'The max items count has been exceeded. The max items count is 10000',
145
- );
146
- });
147
- });
148
-
149
- describe('deleteBulk', () => {
150
- it('should call the delete bulk endpoint', async () => {
151
- const expectedURL =
152
- 'https://test-domain.com/client/v4/accounts' +
153
- '/test-account-id/storage/kv/namespaces/test-namespace-id' +
154
- `/bulk`;
155
-
156
- jest
157
- .spyOn(httpClient, 'sendRequest')
158
- .mockResolvedValueOnce(
159
- new HttpResponse({ statusCode: 200, body: undefined }),
160
- );
161
-
162
- await service.deleteBulk(['someKey']);
163
-
164
- expect(httpClient.sendRequest).toHaveBeenCalledWith({
165
- url: expectedURL,
166
- method: HttpMethod.DELETE,
167
- headers: expectedHeaders,
168
- body: ['someKey'],
169
- } as HttpRequestOptions);
170
- });
171
- });
172
-
173
- it('should not call the delete bulk endpoint if passed empty keys array', async () => {
174
- await service.deleteBulk([]);
175
-
176
- expect(httpClient.sendRequest).toHaveBeenCalledTimes(0);
177
- });
178
- });
@@ -1,74 +0,0 @@
1
- import { config } from 'dotenv';
2
- import * as process from 'process';
3
- import * as path from 'path';
4
- import { injectable } from 'inversify';
5
- import { z, ZodType } from 'zod';
6
- import {
7
- HttpMethod,
8
- HttpRequestBuilderFactory,
9
- } from '@squiz/optimization-http-client';
10
-
11
- export abstract class ConfigurationLoader<TSchema extends ZodType> {
12
- abstract load(): Promise<z.infer<TSchema>>;
13
- }
14
-
15
- @injectable()
16
- export class DotEnvConfigurationLoader<TSchema extends ZodType>
17
- implements ConfigurationLoader<TSchema>
18
- {
19
- constructor(private readonly schema: TSchema) {}
20
-
21
- async load(): Promise<z.infer<TSchema>> {
22
- const output = config({
23
- path: path.join(__dirname, '../../../../env/.env'),
24
- });
25
-
26
- if (output.error) {
27
- throw output.error;
28
- }
29
-
30
- return this.schema.parse(process.env);
31
- }
32
- }
33
-
34
- export type LambdaLayerAppConfigConfigurationLoaderConfig = {
35
- appConfigName: string;
36
- env: string;
37
- configurationName: string;
38
- };
39
-
40
- @injectable()
41
- export class LambdaLayerAppConfigConfigurationLoader<TSchema extends ZodType>
42
- implements ConfigurationLoader<TSchema>
43
- {
44
- constructor(
45
- private readonly opts: LambdaLayerAppConfigConfigurationLoaderConfig,
46
- private readonly httpRequestBuilderFactory: HttpRequestBuilderFactory,
47
- private readonly schema: TSchema,
48
- ) {}
49
-
50
- async load(): Promise<z.infer<TSchema>> {
51
- const configuration = await this.fetchFromApi();
52
-
53
- return this.schema.parse(configuration);
54
- }
55
-
56
- private async fetchFromApi(): Promise<unknown> {
57
- const application = this.opts.appConfigName;
58
- const environment = this.opts.env;
59
- const configuration = this.opts.configurationName;
60
- // the http://localhost:2772 represents the URL to the AWS AppConfig Lambda layer
61
- // the documentation: https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions.html
62
- const url =
63
- `http://localhost:2772` +
64
- `/applications/${application}/environments/${environment}/configurations/${configuration}`;
65
-
66
- const response = await this.httpRequestBuilderFactory
67
- .create()
68
- .url(url)
69
- .method(HttpMethod.GET)
70
- .sendRequest();
71
-
72
- return response.body;
73
- }
74
- }
@@ -1,62 +0,0 @@
1
- import {
2
- ConfigurationLoader,
3
- LambdaLayerAppConfigConfigurationLoader,
4
- LambdaLayerAppConfigConfigurationLoaderConfig,
5
- } from '../ConfigurationLoader';
6
- import {
7
- HttpRequestBuilderFactory,
8
- HttpClient,
9
- HttpMethod,
10
- HttpResponse,
11
- createHttpClientMock,
12
- } from '@squiz/optimization-http-client';
13
- import { faker } from '@faker-js/faker';
14
- import { z } from 'zod';
15
-
16
- describe('LambdaLayerAppConfigConfigurationLoader', () => {
17
- const opts: LambdaLayerAppConfigConfigurationLoaderConfig = {
18
- env: 'local',
19
- appConfigName: 'example-app',
20
- configurationName: 'main',
21
- };
22
- const CONFIG_SCHEMA = z.object({
23
- EXAMPLE_VALUE: z.string(),
24
- });
25
-
26
- type Config = z.infer<typeof CONFIG_SCHEMA>;
27
-
28
- let configurationLoader: ConfigurationLoader<typeof CONFIG_SCHEMA>;
29
- let httpClient: HttpClient;
30
-
31
- beforeEach(() => {
32
- httpClient = createHttpClientMock();
33
- configurationLoader = new LambdaLayerAppConfigConfigurationLoader<
34
- typeof CONFIG_SCHEMA
35
- >(opts, new HttpRequestBuilderFactory(httpClient), CONFIG_SCHEMA);
36
- });
37
-
38
- it('should call AWS AppConfig Layer with passed arguments', async () => {
39
- const expectedResult: Config = {
40
- EXAMPLE_VALUE: faker.word.words(),
41
- };
42
-
43
- jest.spyOn(httpClient, 'sendRequest').mockResolvedValueOnce(
44
- new HttpResponse({
45
- body: expectedResult,
46
- statusCode: 200,
47
- }),
48
- );
49
-
50
- const result = await configurationLoader.load();
51
-
52
- const expectedURL =
53
- `http://localhost:2772` +
54
- `/applications/example-app/environments/local/configurations/main`;
55
-
56
- expect(result).toEqual(expectedResult);
57
- expect(httpClient.sendRequest).toHaveBeenCalledWith({
58
- url: expectedURL,
59
- method: HttpMethod.GET,
60
- });
61
- });
62
- });
@@ -1,29 +0,0 @@
1
- export type DateManipulator = {
2
- addDays(days: number): DateManipulator;
3
- addMonths(months: number): DateManipulator;
4
- valueOf(): Date;
5
- };
6
-
7
- export function DateManipulator(input: Date): DateManipulator {
8
- const addDays = (days: number): DateManipulator => {
9
- const copiedDate = new Date(input);
10
-
11
- copiedDate.setDate(copiedDate.getDate() + days);
12
-
13
- return DateManipulator(copiedDate);
14
- };
15
-
16
- const addMonths = (months: number): DateManipulator => {
17
- const copiedDate = new Date(input);
18
-
19
- copiedDate.setMonth(copiedDate.getMonth() + months);
20
-
21
- return DateManipulator(copiedDate);
22
- };
23
-
24
- return {
25
- addDays,
26
- addMonths,
27
- valueOf: () => new Date(input),
28
- };
29
- }
@@ -1,64 +0,0 @@
1
- import { DateManipulator } from '../DateManipulator';
2
-
3
- describe('DateManipulator', () => {
4
- describe('addDays', () => {
5
- it('should add given days to date', () => {
6
- const result = DateManipulator(new Date('2020-01-01T00:00:00.000Z'))
7
- .addDays(32)
8
- .valueOf();
9
-
10
- expect(result).toEqual(new Date('2020-02-02T00:00:00.000Z'));
11
- });
12
-
13
- it('should not change the date if passed 0 days', () => {
14
- const result = DateManipulator(new Date('2020-01-01T00:00:00.000Z'))
15
- .addDays(0)
16
- .valueOf();
17
-
18
- expect(result).toEqual(new Date('2020-01-01T00:00:00.000Z'));
19
- });
20
-
21
- it('should not mutate the input date', () => {
22
- const originalDate = new Date('2020-01-01T00:00:00.000Z');
23
-
24
- const result = DateManipulator(originalDate).addDays(1).valueOf();
25
-
26
- expect(result).not.toEqual(originalDate);
27
- });
28
- });
29
-
30
- describe('addMonths', () => {
31
- it('should add given days to date', () => {
32
- const result = DateManipulator(new Date('2020-01-01T00:00:00.000Z'))
33
- .addMonths(12)
34
- .valueOf();
35
-
36
- expect(result).toEqual(new Date('2021-01-01T00:00:00.000Z'));
37
- });
38
-
39
- it('should not change the date if passed 0 months', () => {
40
- const result = DateManipulator(new Date('2020-01-01T00:00:00.000Z'))
41
- .addMonths(0)
42
- .valueOf();
43
-
44
- expect(result).toEqual(new Date('2020-01-01T00:00:00.000Z'));
45
- });
46
-
47
- it('should not mutate the input date', () => {
48
- const originalDate = new Date('2020-01-01T00:00:00.000Z');
49
-
50
- const result = DateManipulator(originalDate).addMonths(1).valueOf();
51
-
52
- expect(result).not.toEqual(originalDate);
53
- });
54
- });
55
-
56
- it('should return combined result', () => {
57
- const result = DateManipulator(new Date('2020-01-01T00:00:00.000Z'))
58
- .addMonths(12)
59
- .addDays(1)
60
- .valueOf();
61
-
62
- expect(result).toEqual(new Date('2021-01-02T00:00:00.000Z'));
63
- });
64
- });
@@ -1,5 +0,0 @@
1
- import { DomainEvent } from './DomainEvent';
2
-
3
- export interface AggregateRoot {
4
- events(): Array<DomainEvent>;
5
- }
@@ -1,53 +0,0 @@
1
- import 'reflect-metadata';
2
-
3
- export interface DomainEvent<
4
- TData extends Record<string, unknown> = Record<string, unknown>,
5
- > {
6
- readonly eventId: string;
7
- readonly name: string;
8
- readonly detail: TData;
9
- readonly time: Date;
10
- readonly version: string;
11
- readonly entityId: string;
12
- }
13
-
14
- const DOMAIN_EVENT_SYMBOL = Symbol();
15
-
16
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
- export type DomainEventConstructor = new (...any: Array<any>) => DomainEvent;
18
- export type DomainEventName = string;
19
-
20
- export function getDomainEventType(
21
- name: DomainEventName,
22
- ): DomainEventConstructor | undefined {
23
- const domainEvents = getDomainMetadata();
24
-
25
- return domainEvents.find((d) => d.name === name);
26
- }
27
-
28
- export function getDomainMetadata(): Array<DomainEventConstructor> {
29
- return Reflect.getMetadata(DOMAIN_EVENT_SYMBOL, Object) ?? [];
30
- }
31
-
32
- function setDomainMetadata(domainEvent: DomainEventConstructor): void {
33
- const metadata = getDomainMetadata();
34
- const isDomainEventExist = getDomainEventType(domainEvent.name);
35
-
36
- if (isDomainEventExist) {
37
- throw new Error(
38
- `The DomainEvent Name has been used two times: "${domainEvent.name}". Each event name should be uniq`,
39
- );
40
- }
41
-
42
- Reflect.defineMetadata(
43
- DOMAIN_EVENT_SYMBOL,
44
- [...metadata, domainEvent],
45
- Object,
46
- );
47
- }
48
-
49
- export function AsDomainEvent(): ClassDecorator {
50
- return (target) => {
51
- setDomainMetadata(target as unknown as DomainEventConstructor);
52
- };
53
- }