@openstax/ts-utils 1.32.4 → 1.32.6

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.
Files changed (27) hide show
  1. package/dist/cjs/routing/index.d.ts +20 -2
  2. package/dist/cjs/routing/index.js +10 -1
  3. package/dist/cjs/services/apiGateway/index.d.ts +1 -1
  4. package/dist/cjs/services/documentStore/unversioned/file-system.d.ts +1 -0
  5. package/dist/cjs/services/documentStore/unversioned/file-system.js +18 -8
  6. package/dist/cjs/services/documentStore/versioned/file-system.d.ts +1 -0
  7. package/dist/cjs/services/documentStore/versioned/file-system.js +11 -5
  8. package/dist/cjs/services/fileServer/index.d.ts +12 -0
  9. package/dist/cjs/services/fileServer/localFileServer.js +52 -1
  10. package/dist/cjs/services/fileServer/s3FileServer.d.ts +1 -0
  11. package/dist/cjs/services/fileServer/s3FileServer.js +78 -0
  12. package/dist/cjs/services/searchProvider/openSearch.js +6 -4
  13. package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +1 -1
  14. package/dist/esm/routing/index.d.ts +20 -2
  15. package/dist/esm/routing/index.js +8 -0
  16. package/dist/esm/services/apiGateway/index.d.ts +1 -1
  17. package/dist/esm/services/documentStore/unversioned/file-system.d.ts +1 -0
  18. package/dist/esm/services/documentStore/unversioned/file-system.js +18 -8
  19. package/dist/esm/services/documentStore/versioned/file-system.d.ts +1 -0
  20. package/dist/esm/services/documentStore/versioned/file-system.js +11 -5
  21. package/dist/esm/services/fileServer/index.d.ts +12 -0
  22. package/dist/esm/services/fileServer/localFileServer.js +53 -2
  23. package/dist/esm/services/fileServer/s3FileServer.d.ts +1 -0
  24. package/dist/esm/services/fileServer/s3FileServer.js +76 -1
  25. package/dist/esm/services/searchProvider/openSearch.js +6 -4
  26. package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +1 -1
  27. package/package.json +4 -3
@@ -25,6 +25,20 @@ export type RouteMatchRecord<R> = R extends AnyRoute<R> ? {
25
25
  route: R;
26
26
  params: ParamsForRoute<R>;
27
27
  } : never;
28
+ type Flatten<T> = T extends any ? {
29
+ [K in keyof T]: T[K];
30
+ } : never;
31
+ export type ExternalRoute<R> = R extends Route<infer N, infer P, infer Sa, infer Sr, infer Ri, infer Ro> & infer E ? Route<N, P, Sa, Sr extends {
32
+ payload: any;
33
+ } ? Flatten<Pick<Sr, 'payload'>> : Record<string, never>, Ri, Ro> & Flatten<Omit<E, 'name' | 'path' | 'handler' | 'requestServiceProvider'>> : never;
34
+ /** this utility simplifies the route type to remove stuff that is only
35
+ * relevant internal to the route, like the service types, keeping only
36
+ * the payload type which is necessary for the apiGateway
37
+ *
38
+ * this helps avoid the "type too complicated" error that typescript throws
39
+ * when there are a lot of routes with complex services
40
+ **/
41
+ export declare const routesList: <R>(routes: R[]) => ExternalRoute<R>[];
28
42
  /**
29
43
  * The conditional type for the payload for a given route, `R`. This isn't a route structure, its
30
44
  * a convention based on the request middleware
@@ -140,7 +154,9 @@ export declare const makeCreateRoute: <Sa, Ri, Ex = {}>() => CreateRoute<Sa, Ri,
140
154
  */
141
155
  export declare const makeRenderRouteUrl: <Ru extends {
142
156
  path: string;
143
- }>() => <R extends Ru>(route: R, params: ParamsForRoute<R>, query?: QueryParams) => string;
157
+ }>() => <R>(route: ExternalRoute<R> extends Ru ? R & {
158
+ path: string;
159
+ } : R extends Ru ? R : never, params: ParamsForRoute<R>, query?: QueryParams) => string;
144
160
  /**
145
161
  * A pre-made result from `makeRenderRouteUrl`, this function interpolates parameter and query
146
162
  * arguments into a route path.
@@ -153,7 +169,9 @@ export declare const makeRenderRouteUrl: <Ru extends {
153
169
  * @param query the query parameters to add to the route path
154
170
  * @returns the interpolated route path
155
171
  */
156
- export declare const renderAnyRouteUrl: <R extends any>(route: R, params: ParamsForRoute<R>, query?: QueryParams) => string;
172
+ export declare const renderAnyRouteUrl: <R>(route: ExternalRoute<R> extends any ? R & {
173
+ path: string;
174
+ } : R extends any ? R : never, params: ParamsForRoute<R>, query?: QueryParams) => string;
157
175
  type RequestPathExtractor<Ri> = (request: Ri) => string;
158
176
  type RequestLogExtractor<Ri> = (request: Ri) => JsonCompatibleStruct;
159
177
  type RequestRouteMatcher<Ri, R> = (request: Ri, route: R) => boolean;
@@ -2,6 +2,14 @@ import * as pathToRegexp from 'path-to-regexp';
2
2
  import queryString from 'query-string';
3
3
  import { mapFind, memoize } from '../misc/helpers';
4
4
  import { createConsoleLogger } from '../services/logger/console';
5
+ /** this utility simplifies the route type to remove stuff that is only
6
+ * relevant internal to the route, like the service types, keeping only
7
+ * the payload type which is necessary for the apiGateway
8
+ *
9
+ * this helps avoid the "type too complicated" error that typescript throws
10
+ * when there are a lot of routes with complex services
11
+ **/
12
+ export const routesList = (routes) => routes;
5
13
  /**
6
14
  * Makes a createRoute function that can be used to create routes (this is a factory factory). The
7
15
  * `makeCreateRoute` function is typically called once in the backend and once in the frontend to
@@ -38,7 +38,7 @@ export type ApiClientResponse<Ro> = Ro extends any ? {
38
38
  } : never;
39
39
  export type ExpandRoute<T> = T extends ((...args: infer A) => infer R) & {
40
40
  renderUrl: (...args: infer Ar) => Promise<string>;
41
- } ? (...args: A) => R & {
41
+ } ? ((...args: A) => R) & {
42
42
  renderUrl: (...args: Ar) => Promise<string>;
43
43
  } : never;
44
44
  export type MapRoutesToClient<Ru> = [Ru] extends [AnyRoute<Ru>] ? {
@@ -8,6 +8,7 @@ interface Initializer<C> {
8
8
  export declare const fileSystemUnversionedDocumentStore: <C extends string = "fileSystem">(initializer: Initializer<C>) => <T extends TDocument<T>>() => (configProvider: { [_key in C]: ConfigProviderForConfig<Config>; }) => <K extends keyof T>(_: {}, hashKey: K, options?: {
9
9
  afterWrite?: (item: T) => void | Promise<void>;
10
10
  batchAfterWrite?: (items: T[]) => void | Promise<void>;
11
+ skipAssert?: boolean;
11
12
  }) => {
12
13
  loadAllDocumentsTheBadWay: () => Promise<T[]>;
13
14
  getItemsByField: (key: keyof T, value: T[K], pageKey?: string) => Promise<{
@@ -6,9 +6,11 @@ import { ConflictError, NotFoundError } from '../../../errors';
6
6
  import { ifDefined, isDefined } from '../../../guards';
7
7
  import { assertNoUndefined } from '../fileSystemAssert';
8
8
  export const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvider) => (_, hashKey, options) => {
9
+ var _a;
9
10
  const tableName = resolveConfigValue(configProvider[initializer.configSpace || 'fileSystem'].tableName);
10
11
  const tablePath = tableName.then((table) => path.join(initializer.dataDir, table));
11
12
  const { mkdir, readdir, readFile, writeFile } = ifDefined(initializer.fs, fsModule);
13
+ const skipAssert = (_a = options === null || options === void 0 ? void 0 : options.skipAssert) !== null && _a !== void 0 ? _a : false;
12
14
  const mkTableDir = new Promise((resolve, reject) => tablePath.then((path) => mkdir(path, { recursive: true }, (err) => err && err.code !== 'EEXIST' ? reject(err) : resolve())));
13
15
  const hashFilename = (value) => `${hashValue(value)}.json`;
14
16
  const filePath = async (filename) => path.join(await tablePath, filename);
@@ -43,7 +45,8 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
43
45
  return {
44
46
  loadAllDocumentsTheBadWay,
45
47
  getItemsByField: async (key, value, pageKey) => {
46
- assertNoUndefined(value, [key]);
48
+ if (!skipAssert)
49
+ assertNoUndefined(value, [key]);
47
50
  const pageSize = 10;
48
51
  const items = await loadAllDocumentsTheBadWay();
49
52
  const filteredItems = items.filter((item) => item[key] === value);
@@ -56,17 +59,20 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
56
59
  },
57
60
  batchGetItem: async (ids) => {
58
61
  const items = await Promise.all(ids.map((id) => {
59
- assertNoUndefined(id, ['id']);
62
+ if (!skipAssert)
63
+ assertNoUndefined(id, ['id']);
60
64
  return load(hashFilename(id));
61
65
  }));
62
66
  return items.filter(isDefined);
63
67
  },
64
68
  getItem: (id) => {
65
- assertNoUndefined(id, ['id']);
69
+ if (!skipAssert)
70
+ assertNoUndefined(id, ['id']);
66
71
  return load(hashFilename(id));
67
72
  },
68
73
  incrementItemAttribute: async (id, attribute) => {
69
- assertNoUndefined(id, ['id']);
74
+ if (!skipAssert)
75
+ assertNoUndefined(id, ['id']);
70
76
  const filename = hashFilename(id);
71
77
  const path = await filePath(filename);
72
78
  await mkTableDir;
@@ -97,13 +103,15 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
97
103
  throw new NotFoundError(`Item with ${hashKey.toString()} "${id}" does not exist`);
98
104
  }
99
105
  const newItem = { ...data, ...item };
100
- assertNoUndefined(newItem);
106
+ if (!skipAssert)
107
+ assertNoUndefined(newItem);
101
108
  return new Promise((resolve, reject) => {
102
109
  writeFile(path, JSON.stringify(newItem, null, 2), (err) => err ? reject(err) : resolve(newItem));
103
110
  });
104
111
  },
105
112
  putItem: async (item) => {
106
- assertNoUndefined(item);
113
+ if (!skipAssert)
114
+ assertNoUndefined(item);
107
115
  const path = await filePath(hashFilename(item[hashKey]));
108
116
  await mkTableDir;
109
117
  return new Promise((resolve, reject) => {
@@ -111,7 +119,8 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
111
119
  });
112
120
  },
113
121
  createItem: async (item) => {
114
- assertNoUndefined(item);
122
+ if (!skipAssert)
123
+ assertNoUndefined(item);
115
124
  const hashed = hashFilename(item[hashKey]);
116
125
  const existingItem = await load(hashed);
117
126
  if (existingItem) {
@@ -132,7 +141,8 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
132
141
  // Process items sequentially to ensure consistent conflict detection
133
142
  // Note: concurrency parameter is ignored for filesystem to avoid race conditions
134
143
  for (const item of items) {
135
- assertNoUndefined(item);
144
+ if (!skipAssert)
145
+ assertNoUndefined(item);
136
146
  try {
137
147
  const hashed = hashFilename(item[hashKey]);
138
148
  const existingItem = await load(hashed);
@@ -8,6 +8,7 @@ interface Initializer<C> {
8
8
  }
9
9
  export declare const fileSystemVersionedDocumentStore: <C extends string = "fileSystem">(initializer: Initializer<C>) => <T extends VersionedTDocument<T>>() => (configProvider: { [_key in C]: ConfigProviderForConfig<Config>; }) => <K extends keyof T, A extends undefined | ((...a: any[]) => Promise<VersionedDocumentAuthor>)>(_: {}, hashKey: K, options?: {
10
10
  getAuthor?: A;
11
+ skipAssert?: boolean;
11
12
  }) => {
12
13
  loadAllDocumentsTheBadWay: () => Promise<T[]>;
13
14
  getVersions: (id: T[K], startVersion?: number) => Promise<{
@@ -2,7 +2,9 @@ import { assertNoUndefined } from '../fileSystemAssert';
2
2
  import { fileSystemUnversionedDocumentStore } from '../unversioned/file-system';
3
3
  const PAGE_LIMIT = 5;
4
4
  export const fileSystemVersionedDocumentStore = (initializer) => () => (configProvider) => (_, hashKey, options) => {
5
- const unversionedDocuments = fileSystemUnversionedDocumentStore(initializer)()(configProvider)({}, 'id');
5
+ var _a;
6
+ const skipAssert = (_a = options === null || options === void 0 ? void 0 : options.skipAssert) !== null && _a !== void 0 ? _a : false;
7
+ const unversionedDocuments = fileSystemUnversionedDocumentStore(initializer)()(configProvider)({ skipAssert: skipAssert }, 'id');
6
8
  return {
7
9
  loadAllDocumentsTheBadWay: () => {
8
10
  return unversionedDocuments.loadAllDocumentsTheBadWay().then(documents => documents.map(document => {
@@ -10,7 +12,8 @@ export const fileSystemVersionedDocumentStore = (initializer) => () => (configPr
10
12
  }));
11
13
  },
12
14
  getVersions: async (id, startVersion) => {
13
- assertNoUndefined(id, ['id']);
15
+ if (!skipAssert)
16
+ assertNoUndefined(id, ['id']);
14
17
  const item = await unversionedDocuments.getItem(id);
15
18
  const versions = item === null || item === void 0 ? void 0 : item.items.reverse();
16
19
  if (!versions) {
@@ -25,7 +28,8 @@ export const fileSystemVersionedDocumentStore = (initializer) => () => (configPr
25
28
  };
26
29
  },
27
30
  getItem: async (id, timestamp) => {
28
- assertNoUndefined(id, ['id']);
31
+ if (!skipAssert)
32
+ assertNoUndefined(id, ['id']);
29
33
  const item = await unversionedDocuments.getItem(id);
30
34
  if (timestamp) {
31
35
  return item === null || item === void 0 ? void 0 : item.items.find(version => version.timestamp === timestamp);
@@ -41,7 +45,8 @@ export const fileSystemVersionedDocumentStore = (initializer) => () => (configPr
41
45
  save: async (changes) => {
42
46
  var _a;
43
47
  const document = { ...item, ...changes, timestamp, author };
44
- assertNoUndefined(document);
48
+ if (!skipAssert)
49
+ assertNoUndefined(document);
45
50
  const container = (_a = await unversionedDocuments.getItem(document[hashKey])) !== null && _a !== void 0 ? _a : { id: document[hashKey], items: [] };
46
51
  const updated = { ...container, items: [...container.items, document] };
47
52
  await unversionedDocuments.putItem(updated);
@@ -53,7 +58,8 @@ export const fileSystemVersionedDocumentStore = (initializer) => () => (configPr
53
58
  var _a;
54
59
  const author = (options === null || options === void 0 ? void 0 : options.getAuthor) ? await options.getAuthor(...authorArgs) : authorArgs[0];
55
60
  const document = { ...item, timestamp: new Date().getTime(), author };
56
- assertNoUndefined(document);
61
+ if (!skipAssert)
62
+ assertNoUndefined(document);
57
63
  const container = (_a = await unversionedDocuments.getItem(document[hashKey])) !== null && _a !== void 0 ? _a : { id: document[hashKey], items: [] };
58
64
  const updated = { ...container, items: [...container.items, document] };
59
65
  await unversionedDocuments.putItem(updated);
@@ -13,6 +13,18 @@ export declare const isFolderValue: (thing: any) => thing is FolderValue;
13
13
  export interface FileServerAdapter {
14
14
  putFileContent: (source: FileValue, content: string) => Promise<FileValue>;
15
15
  getSignedViewerUrl: (source: FileValue) => Promise<string>;
16
+ getPublicViewerUrl: (source: FileValue) => Promise<string>;
16
17
  getFileContent: (source: FileValue) => Promise<Buffer>;
18
+ getSignedFileUploadConfig: () => Promise<{
19
+ url: string;
20
+ payload: {
21
+ [key: string]: string;
22
+ };
23
+ }>;
24
+ copyFileTo: (source: FileValue, destinationPath: string) => Promise<FileValue>;
25
+ copyFileToDirectory: (source: FileValue, destinationDirectory: string) => Promise<FileValue>;
26
+ isTemporaryUpload: (source: FileValue) => boolean;
27
+ getFileChecksum: (source: FileValue) => Promise<string>;
28
+ filesEqual: (sourceA: FileValue, sourceB: FileValue) => Promise<boolean>;
17
29
  }
18
30
  export declare const isFileOrFolder: (thing: any) => thing is FileValue | FolderValue;
@@ -1,16 +1,18 @@
1
1
  /* cspell:ignore originalname */
2
+ import crypto from 'crypto';
2
3
  import fs from 'fs';
3
4
  import https from 'https';
4
5
  import path from 'path';
5
6
  import cors from 'cors';
6
7
  import express from 'express';
7
8
  import multer from 'multer';
9
+ import { v4 as uuid } from 'uuid';
8
10
  import { assertString } from '../../assertions';
9
11
  import { resolveConfigValue } from '../../config';
10
12
  import { ifDefined } from '../../guards';
11
- import { once } from '../../misc/helpers';
13
+ import { memoize } from '../../misc/helpers';
12
14
  /* istanbul ignore next */
13
- const startServer = once((port, uploadDir) => {
15
+ const startServer = memoize((port, uploadDir) => {
14
16
  // TODO - re-evaluate the `preservePath` behavior to match whatever s3 does
15
17
  const upload = multer({ dest: uploadDir, preservePath: true });
16
18
  const fileServerApp = express();
@@ -55,6 +57,9 @@ export const localFileServer = (initializer) => (configProvider) => {
55
57
  const getSignedViewerUrl = async (source) => {
56
58
  return `https://${await host}:${await port}/${source.path}`;
57
59
  };
60
+ const getPublicViewerUrl = async (source) => {
61
+ return `https://${await host}:${await port}/${source.path}`;
62
+ };
58
63
  const getFileContent = async (source) => {
59
64
  const filePath = path.join(await fileDir, source.path);
60
65
  return fs.promises.readFile(filePath);
@@ -66,9 +71,55 @@ export const localFileServer = (initializer) => (configProvider) => {
66
71
  await fs.promises.writeFile(filePath, content);
67
72
  return source;
68
73
  };
74
+ const getSignedFileUploadConfig = async () => {
75
+ const prefix = 'uploads/' + uuid();
76
+ return {
77
+ url: `https://${await host}:${await port}/`,
78
+ payload: {
79
+ key: prefix + '/${filename}',
80
+ }
81
+ };
82
+ };
83
+ const copyFileTo = async (source, destinationPath) => {
84
+ const sourcePath = path.join(await fileDir, source.path);
85
+ const destPath = path.join(await fileDir, destinationPath);
86
+ const destDirectory = path.dirname(destPath);
87
+ await fs.promises.mkdir(destDirectory, { recursive: true });
88
+ await fs.promises.copyFile(sourcePath, destPath);
89
+ return {
90
+ ...source,
91
+ path: destinationPath
92
+ };
93
+ };
94
+ const copyFileToDirectory = async (source, destination) => {
95
+ const destinationPath = path.join(destination, source.label);
96
+ return copyFileTo(source, destinationPath);
97
+ };
98
+ const isTemporaryUpload = (source) => {
99
+ return source.path.indexOf('uploads/') === 0;
100
+ };
101
+ const getFileChecksum = async (source) => {
102
+ const filePath = path.join(await fileDir, source.path);
103
+ const fileContent = await fs.promises.readFile(filePath);
104
+ return crypto.createHash('md5').update(fileContent).digest('hex');
105
+ };
106
+ const filesEqual = async (sourceA, sourceB) => {
107
+ const [aSum, bSum] = await Promise.all([
108
+ getFileChecksum(sourceA),
109
+ getFileChecksum(sourceB)
110
+ ]);
111
+ return aSum === bSum;
112
+ };
69
113
  return {
70
114
  getSignedViewerUrl,
115
+ getPublicViewerUrl,
71
116
  getFileContent,
72
117
  putFileContent,
118
+ getSignedFileUploadConfig,
119
+ copyFileTo,
120
+ copyFileToDirectory,
121
+ isTemporaryUpload,
122
+ getFileChecksum,
123
+ filesEqual,
73
124
  };
74
125
  };
@@ -4,6 +4,7 @@ import { FileServerAdapter } from '.';
4
4
  export type Config = {
5
5
  bucketName: string;
6
6
  bucketRegion: string;
7
+ publicViewerDomain?: string;
7
8
  };
8
9
  interface Initializer<C> {
9
10
  configSpace?: C;
@@ -1,6 +1,9 @@
1
1
  /* cspell:ignore presigner */
2
- import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
2
+ import path from 'path';
3
+ import { CopyObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
4
+ import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
3
5
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
6
+ import { v4 as uuid } from 'uuid';
4
7
  import { once } from '../..';
5
8
  import { assertDefined } from '../../assertions';
6
9
  import { resolveConfigValue } from '../../config';
@@ -9,6 +12,9 @@ export const s3FileServer = (initializer) => (configProvider) => {
9
12
  const config = configProvider[ifDefined(initializer.configSpace, 'deployed')];
10
13
  const bucketName = once(() => resolveConfigValue(config.bucketName));
11
14
  const bucketRegion = once(() => resolveConfigValue(config.bucketRegion));
15
+ const publicViewerDomain = once(() => 'publicViewerDomain' in config && config.publicViewerDomain
16
+ ? resolveConfigValue(config.publicViewerDomain)
17
+ : undefined);
12
18
  const s3Service = once(async () => {
13
19
  var _a, _b;
14
20
  const args = { apiVersion: '2012-08-10', region: await bucketRegion() };
@@ -24,6 +30,10 @@ export const s3FileServer = (initializer) => (configProvider) => {
24
30
  expiresIn: 3600, // 1 hour
25
31
  });
26
32
  };
33
+ const getPublicViewerUrl = async (source) => {
34
+ const host = assertDefined(await publicViewerDomain(), new Error(`Tried to get public viewer URL for ${source.path} but no publicViewerDomain configured`));
35
+ return `https://${host}/${source.path}`;
36
+ };
27
37
  const getFileContent = async (source) => {
28
38
  const bucket = await bucketName();
29
39
  const command = new GetObjectCommand({ Bucket: bucket, Key: source.path });
@@ -41,9 +51,74 @@ export const s3FileServer = (initializer) => (configProvider) => {
41
51
  await (await s3Service()).send(command);
42
52
  return source;
43
53
  };
54
+ /*
55
+ * https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_s3_presigned_post.html
56
+ * https://docs.aws.amazon.com/AmazonS3/latest/userguide/HTTPPOSTExamples.html
57
+ * https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html
58
+ */
59
+ const getSignedFileUploadConfig = async () => {
60
+ const prefix = 'uploads/' + uuid();
61
+ const bucket = (await bucketName());
62
+ const Conditions = [
63
+ { acl: 'private' },
64
+ { bucket },
65
+ ['starts-with', '$key', prefix]
66
+ ];
67
+ const defaultFields = {
68
+ acl: 'private',
69
+ };
70
+ const { url, fields } = await createPresignedPost(await s3Service(), {
71
+ Bucket: bucket,
72
+ Key: prefix + '/${filename}',
73
+ Conditions,
74
+ Fields: defaultFields,
75
+ Expires: 3600, // 1 hour
76
+ });
77
+ return {
78
+ url, payload: fields
79
+ };
80
+ };
81
+ const copyFileTo = async (source, destinationPath) => {
82
+ const bucket = (await bucketName());
83
+ const destinationPathWithoutLeadingSlash = destinationPath.replace(/^\//, '');
84
+ const command = new CopyObjectCommand({
85
+ Bucket: bucket,
86
+ Key: destinationPathWithoutLeadingSlash,
87
+ CopySource: path.join(bucket, source.path),
88
+ });
89
+ await (await s3Service()).send(command);
90
+ return {
91
+ ...source,
92
+ path: destinationPathWithoutLeadingSlash
93
+ };
94
+ };
95
+ const copyFileToDirectory = async (source, destination) => {
96
+ const destinationPath = path.join(destination, source.label);
97
+ return copyFileTo(source, destinationPath);
98
+ };
99
+ const isTemporaryUpload = (source) => {
100
+ return source.path.indexOf('uploads/') === 0;
101
+ };
102
+ const getFileChecksum = async (source) => {
103
+ const bucket = (await bucketName());
104
+ const command = new HeadObjectCommand({ Bucket: bucket, Key: source.path });
105
+ const response = await (await s3Service()).send(command);
106
+ return assertDefined(response.ETag);
107
+ };
108
+ const filesEqual = async (sourceA, sourceB) => {
109
+ const [aSum, bSum] = await Promise.all([getFileChecksum(sourceA), getFileChecksum(sourceB)]);
110
+ return aSum === bSum;
111
+ };
44
112
  return {
45
113
  getFileContent,
46
114
  putFileContent,
47
115
  getSignedViewerUrl,
116
+ getPublicViewerUrl,
117
+ getSignedFileUploadConfig,
118
+ copyFileTo,
119
+ copyFileToDirectory,
120
+ isTemporaryUpload,
121
+ getFileChecksum,
122
+ filesEqual,
48
123
  };
49
124
  };
@@ -26,10 +26,6 @@ export const openSearchService = (initializer = {}) => (configProvider) => {
26
26
  maxRetries: 4, // default is 3
27
27
  requestTimeout: 5000, // default is 30000
28
28
  pingTimeout: 2000, // default is 30000
29
- sniffOnConnectionFault: true,
30
- sniffOnStart: true,
31
- resurrectStrategy: 'ping',
32
- agent: { keepAlive: false },
33
29
  node: await resolveConfigValue(config.node),
34
30
  }));
35
31
  return (indexConfig) => {
@@ -65,6 +61,9 @@ export const openSearchService = (initializer = {}) => (configProvider) => {
65
61
  body: params.body,
66
62
  id: params.id,
67
63
  refresh: true
64
+ }, {
65
+ requestTimeout: 10000,
66
+ maxRetries: 1,
68
67
  });
69
68
  };
70
69
  const bulkIndex = async (items) => {
@@ -76,6 +75,9 @@ export const openSearchService = (initializer = {}) => (configProvider) => {
76
75
  item.body
77
76
  ]),
78
77
  refresh: true
78
+ }, {
79
+ requestTimeout: 10000,
80
+ maxRetries: 1,
79
81
  });
80
82
  };
81
83
  const search = async (options) => {