@openstax/ts-utils 1.0.0
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/README.md +118 -0
- package/dist/README.md +118 -0
- package/dist/assertions.d.ts +9 -0
- package/dist/assertions.js +99 -0
- package/dist/aws/securityTokenService.d.ts +2 -0
- package/dist/aws/securityTokenService.js +6 -0
- package/dist/aws/ssmService.d.ts +2 -0
- package/dist/aws/ssmService.js +6 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.js +110 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +32 -0
- package/dist/fetch.d.ts +59 -0
- package/dist/fetch.js +47 -0
- package/dist/guards.d.ts +5 -0
- package/dist/guards.js +34 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +120 -0
- package/dist/middleware.d.ts +9 -0
- package/dist/middleware.js +38 -0
- package/dist/package.json +65 -0
- package/dist/pagination.d.ts +65 -0
- package/dist/pagination.js +83 -0
- package/dist/routing.d.ts +100 -0
- package/dist/routing.js +237 -0
- package/dist/services/apiGateway/index.d.ts +61 -0
- package/dist/services/apiGateway/index.js +70 -0
- package/dist/services/authProvider/browser.d.ts +17 -0
- package/dist/services/authProvider/browser.js +30 -0
- package/dist/services/authProvider/decryption.d.ts +16 -0
- package/dist/services/authProvider/decryption.js +56 -0
- package/dist/services/authProvider/index.d.ts +37 -0
- package/dist/services/authProvider/index.js +17 -0
- package/dist/services/authProvider/subrequest.d.ts +16 -0
- package/dist/services/authProvider/subrequest.js +39 -0
- package/dist/services/exercisesGateway/index.d.ts +80 -0
- package/dist/services/exercisesGateway/index.js +94 -0
- package/dist/services/lrsGateway/attempt-utils.d.ts +60 -0
- package/dist/services/lrsGateway/attempt-utils.js +270 -0
- package/dist/services/lrsGateway/file-system.d.ts +15 -0
- package/dist/services/lrsGateway/file-system.js +126 -0
- package/dist/services/lrsGateway/index.d.ts +110 -0
- package/dist/services/lrsGateway/index.js +116 -0
- package/dist/services/searchProvider/index.d.ts +20 -0
- package/dist/services/searchProvider/index.js +2 -0
- package/dist/services/searchProvider/memorySearchTheBadWay.d.ts +12 -0
- package/dist/services/searchProvider/memorySearchTheBadWay.js +51 -0
- package/dist/services/versionedDocumentStore/dynamodb.d.ts +20 -0
- package/dist/services/versionedDocumentStore/dynamodb.js +151 -0
- package/dist/services/versionedDocumentStore/file-system.d.ts +22 -0
- package/dist/services/versionedDocumentStore/file-system.js +112 -0
- package/dist/services/versionedDocumentStore/index.d.ts +23 -0
- package/dist/services/versionedDocumentStore/index.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +2 -0
- package/package.json +65 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { ConfigProviderForConfig } from '../../config';
|
|
2
|
+
import { GenericFetch } from '../../fetch';
|
|
3
|
+
import { WithRequired } from '../../types';
|
|
4
|
+
import { AuthProvider } from '../authProvider';
|
|
5
|
+
declare type Config = {
|
|
6
|
+
lrsHost: string;
|
|
7
|
+
lrsAuthorization: string;
|
|
8
|
+
};
|
|
9
|
+
interface Initializer<C> {
|
|
10
|
+
configSpace?: C;
|
|
11
|
+
fetch: GenericFetch;
|
|
12
|
+
}
|
|
13
|
+
export interface XapiStatement {
|
|
14
|
+
actor: {
|
|
15
|
+
account: {
|
|
16
|
+
homePage: 'https://openstax.org';
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
objectType: 'Agent';
|
|
20
|
+
};
|
|
21
|
+
id: string;
|
|
22
|
+
context?: {
|
|
23
|
+
contextActivities?: {
|
|
24
|
+
parent?: [
|
|
25
|
+
{
|
|
26
|
+
id: string;
|
|
27
|
+
objectType: 'Activity';
|
|
28
|
+
}
|
|
29
|
+
];
|
|
30
|
+
};
|
|
31
|
+
statement?: {
|
|
32
|
+
objectType: 'StatementRef';
|
|
33
|
+
id: string;
|
|
34
|
+
};
|
|
35
|
+
registration?: string;
|
|
36
|
+
platform?: string;
|
|
37
|
+
};
|
|
38
|
+
object: {
|
|
39
|
+
definition?: {
|
|
40
|
+
extensions?: {
|
|
41
|
+
[key: string]: string;
|
|
42
|
+
};
|
|
43
|
+
name: {
|
|
44
|
+
[key: string]: string;
|
|
45
|
+
};
|
|
46
|
+
type: string;
|
|
47
|
+
};
|
|
48
|
+
id: string;
|
|
49
|
+
objectType: 'Activity';
|
|
50
|
+
};
|
|
51
|
+
result?: {
|
|
52
|
+
score?: {
|
|
53
|
+
scaled?: number;
|
|
54
|
+
raw?: number;
|
|
55
|
+
min?: number;
|
|
56
|
+
max?: number;
|
|
57
|
+
};
|
|
58
|
+
success?: boolean;
|
|
59
|
+
completion?: boolean;
|
|
60
|
+
response?: string;
|
|
61
|
+
duration?: string;
|
|
62
|
+
extensions?: {
|
|
63
|
+
[key: string]: string;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
verb: {
|
|
67
|
+
display?: {
|
|
68
|
+
[key: string]: string;
|
|
69
|
+
};
|
|
70
|
+
id: string;
|
|
71
|
+
};
|
|
72
|
+
timestamp: string;
|
|
73
|
+
stored?: string;
|
|
74
|
+
}
|
|
75
|
+
export declare type SavedXapiStatement = WithRequired<XapiStatement, 'stored'>;
|
|
76
|
+
export declare type EagerXapiStatement = Omit<XapiStatement, 'stored'>;
|
|
77
|
+
export declare type LrsGateway = ReturnType<ReturnType<ReturnType<typeof lrsGateway>>>;
|
|
78
|
+
export declare type LrsProvider = ReturnType<ReturnType<typeof lrsGateway>>;
|
|
79
|
+
export declare const lrsGateway: <C extends string = "lrs">(initializer: Initializer<C>) => (configProvider: { [key in C]: {
|
|
80
|
+
lrsHost: import("../../config").ConfigValueProvider<string>;
|
|
81
|
+
lrsAuthorization: import("../../config").ConfigValueProvider<string>;
|
|
82
|
+
}; }) => (authProvider: AuthProvider) => {
|
|
83
|
+
putXapiStatements: (statements: Array<Pick<XapiStatement, 'object' | 'verb' | 'context' | 'result'> & {
|
|
84
|
+
id?: string;
|
|
85
|
+
}>) => Promise<EagerXapiStatement[]>;
|
|
86
|
+
getXapiStatements: ({ user, anyUser, ...options }: {
|
|
87
|
+
verb?: string | undefined;
|
|
88
|
+
activity?: string | undefined;
|
|
89
|
+
registration?: string | undefined;
|
|
90
|
+
related_activities?: boolean | undefined;
|
|
91
|
+
user?: string | undefined;
|
|
92
|
+
anyUser?: boolean | undefined;
|
|
93
|
+
}) => Promise<{
|
|
94
|
+
more: string;
|
|
95
|
+
statements: XapiStatement[];
|
|
96
|
+
}>;
|
|
97
|
+
getMoreXapiStatements: (more: string) => Promise<{
|
|
98
|
+
more: string;
|
|
99
|
+
statements: XapiStatement[];
|
|
100
|
+
}>;
|
|
101
|
+
getAllXapiStatements: (args_0: {
|
|
102
|
+
verb?: string | undefined;
|
|
103
|
+
activity?: string | undefined;
|
|
104
|
+
registration?: string | undefined;
|
|
105
|
+
related_activities?: boolean | undefined;
|
|
106
|
+
user?: string | undefined;
|
|
107
|
+
anyUser?: boolean | undefined;
|
|
108
|
+
}) => Promise<XapiStatement[]>;
|
|
109
|
+
};
|
|
110
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.lrsGateway = void 0;
|
|
30
|
+
const formatISO_1 = __importDefault(require("date-fns/formatISO"));
|
|
31
|
+
const queryString = __importStar(require("query-string"));
|
|
32
|
+
const assertions_1 = require("../../assertions");
|
|
33
|
+
const config_1 = require("../../config");
|
|
34
|
+
const errors_1 = require("../../errors");
|
|
35
|
+
const guards_1 = require("../../guards");
|
|
36
|
+
const routing_1 = require("../../routing");
|
|
37
|
+
const lrsGateway = (initializer) => (configProvider) => {
|
|
38
|
+
const config = configProvider[(0, guards_1.ifDefined)(initializer.configSpace, 'lrs')];
|
|
39
|
+
const lrsHost = (0, config_1.resolveConfigValue)(config.lrsHost);
|
|
40
|
+
const lrsAuthorization = (0, config_1.resolveConfigValue)(config.lrsAuthorization);
|
|
41
|
+
return (authProvider) => {
|
|
42
|
+
const putXapiStatements = async (statements) => {
|
|
43
|
+
const user = (0, assertions_1.assertDefined)(await authProvider.getUser(), new errors_1.UnauthorizedError);
|
|
44
|
+
const statementsWithDefaults = statements.map(statement => ({
|
|
45
|
+
...statement,
|
|
46
|
+
actor: {
|
|
47
|
+
account: {
|
|
48
|
+
homePage: 'https://openstax.org',
|
|
49
|
+
name: user.uuid,
|
|
50
|
+
},
|
|
51
|
+
objectType: 'Agent',
|
|
52
|
+
},
|
|
53
|
+
timestamp: (0, formatISO_1.default)(new Date())
|
|
54
|
+
}));
|
|
55
|
+
return initializer.fetch((await lrsHost).replace(/\/+$/, '') + '/data/xAPI/statements', {
|
|
56
|
+
body: JSON.stringify(statementsWithDefaults),
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: await lrsAuthorization,
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
'X-Experience-API-Version': '1.0.0',
|
|
61
|
+
},
|
|
62
|
+
method: routing_1.METHOD.POST,
|
|
63
|
+
})
|
|
64
|
+
.then(response => response.json())
|
|
65
|
+
.then(ids => ids.map((id, index) => ({ id, ...statementsWithDefaults[index] })));
|
|
66
|
+
};
|
|
67
|
+
const getMoreXapiStatements = async (more) => {
|
|
68
|
+
return initializer.fetch((await lrsHost).replace(/\/+$/, '') + more, {
|
|
69
|
+
headers: {
|
|
70
|
+
Authorization: await lrsAuthorization,
|
|
71
|
+
'X-Experience-API-Version': '1.0.0',
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
.then(response => response.json())
|
|
75
|
+
.then(json => json);
|
|
76
|
+
};
|
|
77
|
+
const getXapiStatements = async ({ user, anyUser, ...options }) => {
|
|
78
|
+
return initializer.fetch((await lrsHost).replace(/\/+$/, '') + '/data/xAPI/statements?' + queryString.stringify({
|
|
79
|
+
...options,
|
|
80
|
+
...(anyUser === true ? {} : {
|
|
81
|
+
agent: JSON.stringify({
|
|
82
|
+
account: {
|
|
83
|
+
homePage: 'https://openstax.org',
|
|
84
|
+
name: user || (0, assertions_1.assertDefined)(await authProvider.getUser(), new errors_1.UnauthorizedError()).uuid,
|
|
85
|
+
},
|
|
86
|
+
objectType: 'Agent',
|
|
87
|
+
}),
|
|
88
|
+
})
|
|
89
|
+
}), {
|
|
90
|
+
headers: {
|
|
91
|
+
Authorization: await lrsAuthorization,
|
|
92
|
+
'X-Experience-API-Version': '1.0.0',
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
.then(response => response.json())
|
|
96
|
+
.then(json => json);
|
|
97
|
+
};
|
|
98
|
+
const getAllXapiStatements = (...args) => {
|
|
99
|
+
const loadRemaining = async (result) => {
|
|
100
|
+
if (!result.more) {
|
|
101
|
+
return result.statements;
|
|
102
|
+
}
|
|
103
|
+
const { more, statements } = await getMoreXapiStatements(result.more);
|
|
104
|
+
return loadRemaining({ more, statements: [...result.statements, ...statements] });
|
|
105
|
+
};
|
|
106
|
+
return getXapiStatements(...args).then(loadRemaining);
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
putXapiStatements,
|
|
110
|
+
getXapiStatements,
|
|
111
|
+
getMoreXapiStatements,
|
|
112
|
+
getAllXapiStatements,
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
exports.lrsGateway = lrsGateway;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
declare type KeysMatching<T, V> = {
|
|
2
|
+
[K in keyof T]-?: T[K] extends V ? K : never;
|
|
3
|
+
}[keyof T];
|
|
4
|
+
declare type FilterFieldTypes = string;
|
|
5
|
+
declare type Filter<F extends keyof T, T> = F extends KeysMatching<T, FilterFieldTypes> ? {
|
|
6
|
+
key: F;
|
|
7
|
+
value: T[F];
|
|
8
|
+
} : never;
|
|
9
|
+
declare type QueryFieldTypes = string | undefined | null;
|
|
10
|
+
declare type Field<F extends keyof T, T> = F extends KeysMatching<T, QueryFieldTypes> ? {
|
|
11
|
+
key: F;
|
|
12
|
+
weight: number;
|
|
13
|
+
} : never;
|
|
14
|
+
export interface SearchOptions<T> {
|
|
15
|
+
page?: number;
|
|
16
|
+
query: string | undefined;
|
|
17
|
+
fields: Field<keyof T, T>[];
|
|
18
|
+
filter?: Filter<keyof T, T>[];
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SearchOptions } from '.';
|
|
2
|
+
export declare const memorySearchTheBadWay: <T>({ loadAllDocumentsTheBadWay }: {
|
|
3
|
+
loadAllDocumentsTheBadWay: () => Promise<T[]>;
|
|
4
|
+
}) => {
|
|
5
|
+
search: (options: SearchOptions<T>) => Promise<{
|
|
6
|
+
items: T[];
|
|
7
|
+
pageSize: number;
|
|
8
|
+
currentPage: number;
|
|
9
|
+
totalItems: number;
|
|
10
|
+
totalPages: number;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.memorySearchTheBadWay = void 0;
|
|
4
|
+
const guards_1 = require("../../guards");
|
|
5
|
+
const MIN_MATCH = 0.5;
|
|
6
|
+
const MAX_RESULTS = 10;
|
|
7
|
+
const memorySearchTheBadWay = ({ loadAllDocumentsTheBadWay }) => {
|
|
8
|
+
return {
|
|
9
|
+
search: async (options) => {
|
|
10
|
+
const results = (await loadAllDocumentsTheBadWay())
|
|
11
|
+
.map(document => {
|
|
12
|
+
let weight = 0;
|
|
13
|
+
if (options.query !== undefined) {
|
|
14
|
+
for (const field of options.fields) {
|
|
15
|
+
const value = document[field.key];
|
|
16
|
+
if (value === undefined || value === null) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
if (value.indexOf(options.query) !== -1) {
|
|
21
|
+
weight += (1 * field.weight);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
throw new Error(`invalid value type when searching in field ${field.key.toString()}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const field of options.filter || []) {
|
|
30
|
+
if (document[field.key] !== field.value) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { document, weight };
|
|
35
|
+
})
|
|
36
|
+
.filter(guards_1.isDefined)
|
|
37
|
+
.filter(r => !options.query || r.weight >= MIN_MATCH);
|
|
38
|
+
results.sort((a, b) => b.weight - a.weight);
|
|
39
|
+
const page = options.page || 1;
|
|
40
|
+
const offset = (page - 1) * MAX_RESULTS;
|
|
41
|
+
return {
|
|
42
|
+
items: results.slice(offset, offset + MAX_RESULTS).map(r => r.document),
|
|
43
|
+
pageSize: MAX_RESULTS,
|
|
44
|
+
currentPage: page,
|
|
45
|
+
totalItems: results.length,
|
|
46
|
+
totalPages: Math.ceil(results.length / MAX_RESULTS)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
exports.memorySearchTheBadWay = memorySearchTheBadWay;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ConfigProviderForConfig } from '../../config';
|
|
2
|
+
import { TDocument, VersionedDocumentAuthor } from '.';
|
|
3
|
+
declare type DynamoConfig = {
|
|
4
|
+
tableName: string;
|
|
5
|
+
};
|
|
6
|
+
interface Initializer<C> {
|
|
7
|
+
configSpace?: C;
|
|
8
|
+
}
|
|
9
|
+
export declare const dynamoVersionedDocumentStore: <C extends string = "dynamodb">(initializer?: Initializer<C> | undefined) => <T extends TDocument<T>>() => (configProvider: { [key in C]: {
|
|
10
|
+
tableName: import("../../config").ConfigValueProvider<string>;
|
|
11
|
+
}; }) => <K extends keyof T, A extends ((...a: any[]) => Promise<VersionedDocumentAuthor>) | undefined>(hashKey: K, getAuthor: A) => {
|
|
12
|
+
loadAllDocumentsTheBadWay: () => Promise<T[]>;
|
|
13
|
+
getVersions: (id: T[K], startVersion?: number | undefined) => Promise<{
|
|
14
|
+
items: T[];
|
|
15
|
+
nextPageToken: number | undefined;
|
|
16
|
+
} | undefined>;
|
|
17
|
+
getItem: (id: T[K], timestamp?: number | undefined) => Promise<T | undefined>;
|
|
18
|
+
putItem: (item: Omit<T, "timestamp" | "author">, ...authorArgs: A extends Function ? Parameters<A> : [VersionedDocumentAuthor]) => Promise<T>;
|
|
19
|
+
};
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dynamoVersionedDocumentStore = void 0;
|
|
4
|
+
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
|
|
5
|
+
const __1 = require("../..");
|
|
6
|
+
const config_1 = require("../../config");
|
|
7
|
+
const guards_1 = require("../../guards");
|
|
8
|
+
const dynamodb = (0, __1.once)(() => new client_dynamodb_1.DynamoDB({ apiVersion: '2012-08-10' }));
|
|
9
|
+
const encodeDynamoAttribute = (value) => {
|
|
10
|
+
if (typeof value === 'string') {
|
|
11
|
+
return { S: value };
|
|
12
|
+
}
|
|
13
|
+
if (typeof value === 'number') {
|
|
14
|
+
return { N: value.toString() };
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === 'boolean') {
|
|
17
|
+
return { BOOL: value };
|
|
18
|
+
}
|
|
19
|
+
if (value === null) {
|
|
20
|
+
return { NULL: true };
|
|
21
|
+
}
|
|
22
|
+
if (value instanceof Array) {
|
|
23
|
+
return { L: value.map(encodeDynamoAttribute) };
|
|
24
|
+
}
|
|
25
|
+
if ((0, guards_1.isPlainObject)(value)) {
|
|
26
|
+
return { M: encodeDynamoDocument(value) };
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`unknown attribute type ${typeof value} with value ${value}.`);
|
|
29
|
+
};
|
|
30
|
+
const encodeDynamoDocument = (base) => Object.fromEntries(Object.entries(base).map(([key, value]) => ([key, encodeDynamoAttribute(value)])));
|
|
31
|
+
const decodeDynamoAttribute = (value) => {
|
|
32
|
+
if (value.S !== undefined) {
|
|
33
|
+
return value.S;
|
|
34
|
+
}
|
|
35
|
+
if (value.N !== undefined) {
|
|
36
|
+
return parseFloat(value.N);
|
|
37
|
+
}
|
|
38
|
+
if (value.BOOL !== undefined) {
|
|
39
|
+
return value.BOOL;
|
|
40
|
+
}
|
|
41
|
+
if (value.NULL !== undefined) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (value.L !== undefined) {
|
|
45
|
+
return value.L.map(decodeDynamoAttribute);
|
|
46
|
+
}
|
|
47
|
+
if (value.M !== undefined) {
|
|
48
|
+
return decodeDynamoDocument(value.M);
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`unknown attribute type: ${JSON.stringify(value)}.`);
|
|
51
|
+
};
|
|
52
|
+
const decodeDynamoDocument = (document) => Object.fromEntries(Object.entries(document).map(([key, value]) => ([key, decodeDynamoAttribute(value)])));
|
|
53
|
+
// i'm not really excited about getAuthor being required, but ts is getting confused about the type when unspecified
|
|
54
|
+
const dynamoVersionedDocumentStore = (initializer) => () => (configProvider) => (hashKey, getAuthor) => {
|
|
55
|
+
const options = (0, guards_1.ifDefined)(initializer, {});
|
|
56
|
+
const tableName = (0, config_1.resolveConfigValue)(configProvider[(0, guards_1.ifDefined)(options.configSpace, 'dynamodb')].tableName);
|
|
57
|
+
return {
|
|
58
|
+
loadAllDocumentsTheBadWay: async () => {
|
|
59
|
+
const loadAllResults = async (ExclusiveStartKey) => {
|
|
60
|
+
var _a;
|
|
61
|
+
const cmd = new client_dynamodb_1.ScanCommand({ TableName: await tableName, ExclusiveStartKey });
|
|
62
|
+
const result = await dynamodb().send(cmd);
|
|
63
|
+
const resultItems = ((_a = result.Items) === null || _a === void 0 ? void 0 : _a.map(decodeDynamoDocument)) || [];
|
|
64
|
+
if (result.LastEvaluatedKey) {
|
|
65
|
+
return [...resultItems, ...await loadAllResults(result.LastEvaluatedKey)];
|
|
66
|
+
}
|
|
67
|
+
return resultItems;
|
|
68
|
+
};
|
|
69
|
+
const allResults = await loadAllResults().then(results => results.reduce((result, document) => {
|
|
70
|
+
const current = result.get(document[hashKey]);
|
|
71
|
+
if (!current || current.timestamp < document.timestamp) {
|
|
72
|
+
return result.set(document[hashKey], document);
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}, new Map()));
|
|
76
|
+
return Array.from(allResults.values());
|
|
77
|
+
},
|
|
78
|
+
getVersions: async (id, startVersion) => {
|
|
79
|
+
const cmd = new client_dynamodb_1.QueryCommand({
|
|
80
|
+
TableName: await tableName,
|
|
81
|
+
KeyConditionExpression: '#hk = :hkv',
|
|
82
|
+
ExpressionAttributeValues: {
|
|
83
|
+
':hkv': encodeDynamoAttribute(id)
|
|
84
|
+
},
|
|
85
|
+
ExpressionAttributeNames: {
|
|
86
|
+
'#hk': hashKey.toString()
|
|
87
|
+
},
|
|
88
|
+
...(startVersion
|
|
89
|
+
? { ExclusiveStartKey: {
|
|
90
|
+
[hashKey]: encodeDynamoAttribute(id),
|
|
91
|
+
timestamp: { N: startVersion.toString() }
|
|
92
|
+
} }
|
|
93
|
+
: {}),
|
|
94
|
+
ScanIndexForward: false,
|
|
95
|
+
});
|
|
96
|
+
return dynamodb().send(cmd).then(result => {
|
|
97
|
+
var _a;
|
|
98
|
+
const items = (_a = result.Items) === null || _a === void 0 ? void 0 : _a.map(decodeDynamoDocument);
|
|
99
|
+
if (!items || items.length === 0) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
items,
|
|
104
|
+
nextPageToken: result.LastEvaluatedKey ? decodeDynamoDocument(result.LastEvaluatedKey).timestamp : undefined
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
getItem: async (id, timestamp) => {
|
|
109
|
+
let keyConditionExpression = '#hk = :hkv';
|
|
110
|
+
const expressionAttributeNames = {
|
|
111
|
+
'#hk': hashKey.toString()
|
|
112
|
+
};
|
|
113
|
+
const expressionAttributeValues = {
|
|
114
|
+
':hkv': encodeDynamoAttribute(id)
|
|
115
|
+
};
|
|
116
|
+
if (timestamp) {
|
|
117
|
+
keyConditionExpression += ' and #ts = :tsv';
|
|
118
|
+
expressionAttributeNames['#ts'] = 'timestamp';
|
|
119
|
+
expressionAttributeValues[':tsv'] = encodeDynamoAttribute(timestamp);
|
|
120
|
+
}
|
|
121
|
+
const cmd = new client_dynamodb_1.QueryCommand({
|
|
122
|
+
TableName: await tableName,
|
|
123
|
+
KeyConditionExpression: keyConditionExpression,
|
|
124
|
+
ExpressionAttributeNames: expressionAttributeNames,
|
|
125
|
+
ExpressionAttributeValues: expressionAttributeValues,
|
|
126
|
+
ScanIndexForward: false,
|
|
127
|
+
Limit: 1
|
|
128
|
+
});
|
|
129
|
+
return dynamodb().send(cmd).then(result => {
|
|
130
|
+
var _a;
|
|
131
|
+
return (_a = result.Items) === null || _a === void 0 ? void 0 : _a.map(decodeDynamoDocument)[0];
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
putItem: async (item, ...authorArgs) => {
|
|
135
|
+
// this getAuthor type is terrible
|
|
136
|
+
const author = getAuthor ? await getAuthor(...authorArgs) : authorArgs[0];
|
|
137
|
+
const document = {
|
|
138
|
+
...item,
|
|
139
|
+
timestamp: new Date().getTime(),
|
|
140
|
+
author
|
|
141
|
+
};
|
|
142
|
+
const cmd = new client_dynamodb_1.PutItemCommand({
|
|
143
|
+
TableName: await tableName,
|
|
144
|
+
Item: encodeDynamoDocument(document),
|
|
145
|
+
});
|
|
146
|
+
return dynamodb().send(cmd)
|
|
147
|
+
.then(() => document);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
exports.dynamoVersionedDocumentStore = dynamoVersionedDocumentStore;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ConfigProviderForConfig } from '../../config';
|
|
2
|
+
import { TDocument, VersionedDocumentAuthor } from '.';
|
|
3
|
+
declare type Config = {
|
|
4
|
+
tableName: string;
|
|
5
|
+
};
|
|
6
|
+
interface Initializer<C> {
|
|
7
|
+
dataDir: string;
|
|
8
|
+
fs?: Pick<typeof import('fs'), 'readFile' | 'writeFile'>;
|
|
9
|
+
configSpace?: C;
|
|
10
|
+
}
|
|
11
|
+
export declare const fileSystemVersionedDocumentStore: <C extends string = "fileSystem">(initializer: Initializer<C>) => <T extends TDocument<T>>() => (configProvider: { [key in C]: {
|
|
12
|
+
tableName: import("../../config").ConfigValueProvider<string>;
|
|
13
|
+
}; }) => <K extends keyof T, A extends ((...a: any[]) => Promise<VersionedDocumentAuthor>) | undefined>(hashKey: K, getAuthor: A) => {
|
|
14
|
+
loadAllDocumentsTheBadWay: () => Promise<T[]>;
|
|
15
|
+
getVersions: (id: T[K], startVersion?: number | undefined) => Promise<{
|
|
16
|
+
items: T[];
|
|
17
|
+
nextPageToken: number | undefined;
|
|
18
|
+
} | undefined>;
|
|
19
|
+
getItem: (id: T[K], timestamp?: number | undefined) => Promise<T | undefined>;
|
|
20
|
+
putItem: (item: Omit<T, "timestamp" | "author">, ...authorArgs: A extends Function ? Parameters<A> : [VersionedDocumentAuthor]) => Promise<T>;
|
|
21
|
+
};
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.fileSystemVersionedDocumentStore = void 0;
|
|
30
|
+
const fsModule = __importStar(require("fs"));
|
|
31
|
+
const path_1 = __importDefault(require("path"));
|
|
32
|
+
const __1 = require("../..");
|
|
33
|
+
const config_1 = require("../../config");
|
|
34
|
+
const guards_1 = require("../../guards");
|
|
35
|
+
const PAGE_LIMIT = 5;
|
|
36
|
+
const fileSystemVersionedDocumentStore = (initializer) => () => (configProvider) => (hashKey, getAuthor) => {
|
|
37
|
+
const tableName = (0, config_1.resolveConfigValue)(configProvider[initializer.configSpace || 'fileSystem'].tableName);
|
|
38
|
+
const filePath = tableName.then((table) => path_1.default.join(initializer.dataDir, table));
|
|
39
|
+
const { readFile, writeFile } = (0, guards_1.ifDefined)(initializer.fs, fsModule);
|
|
40
|
+
let data;
|
|
41
|
+
const load = filePath.then(path => new Promise(resolve => {
|
|
42
|
+
readFile(path, (err, readData) => {
|
|
43
|
+
if (err) {
|
|
44
|
+
console.error(err);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
try {
|
|
48
|
+
data = JSON.parse(readData.toString());
|
|
49
|
+
if (typeof data !== 'object') {
|
|
50
|
+
data = undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
console.error(e);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
resolve();
|
|
58
|
+
});
|
|
59
|
+
}));
|
|
60
|
+
let previousSave;
|
|
61
|
+
return {
|
|
62
|
+
loadAllDocumentsTheBadWay: async () => {
|
|
63
|
+
await load;
|
|
64
|
+
return Object.entries(data || []).map(([, versions]) => {
|
|
65
|
+
const versionsList = Object.values(versions);
|
|
66
|
+
return versionsList[versionsList.length - 1];
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
getVersions: async (id, startVersion) => {
|
|
70
|
+
await load;
|
|
71
|
+
const versions = data === null || data === void 0 ? void 0 : data[(0, __1.hashValue)(id)];
|
|
72
|
+
const versionsList = versions ? Object.values(versions) : undefined;
|
|
73
|
+
if (!versions || !versionsList) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const startIndex = startVersion ? versionsList.indexOf(versions[startVersion.toString()]) + 1 : 0;
|
|
77
|
+
const items = versionsList.slice(startIndex, startIndex + PAGE_LIMIT);
|
|
78
|
+
const hasMore = (startIndex + 5) < versionsList.length;
|
|
79
|
+
return {
|
|
80
|
+
items,
|
|
81
|
+
nextPageToken: hasMore ? items[items.length - 1].timestamp : undefined
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
getItem: async (id, timestamp) => {
|
|
85
|
+
await load;
|
|
86
|
+
const versions = data === null || data === void 0 ? void 0 : data[(0, __1.hashValue)(id)];
|
|
87
|
+
if (timestamp) {
|
|
88
|
+
return versions === null || versions === void 0 ? void 0 : versions[timestamp];
|
|
89
|
+
}
|
|
90
|
+
const versionsList = versions ? Object.values(versions) : [];
|
|
91
|
+
return versionsList[versionsList.length - 1];
|
|
92
|
+
},
|
|
93
|
+
putItem: async (item, ...authorArgs) => {
|
|
94
|
+
await load;
|
|
95
|
+
await previousSave;
|
|
96
|
+
// this getAuthor type is terrible
|
|
97
|
+
const author = getAuthor ? await getAuthor(...authorArgs) : authorArgs[0];
|
|
98
|
+
const document = { ...item, timestamp: new Date().getTime(), author };
|
|
99
|
+
const hash = (0, __1.hashValue)(document[hashKey]);
|
|
100
|
+
const path = await filePath;
|
|
101
|
+
const save = previousSave = new Promise(resolve => {
|
|
102
|
+
data = data || {};
|
|
103
|
+
const versions = data[hash] = data[hash] || {};
|
|
104
|
+
versions[document.timestamp] = document;
|
|
105
|
+
writeFile(path, JSON.stringify(data, null, 2), () => resolve());
|
|
106
|
+
});
|
|
107
|
+
await save;
|
|
108
|
+
return document;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
exports.fileSystemVersionedDocumentStore = fileSystemVersionedDocumentStore;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { dynamoVersionedDocumentStore } from './dynamodb';
|
|
2
|
+
export declare type DocumentBaseValueTypes = number | boolean | string | null | DocumentBaseMapType | DocumentBaseListType;
|
|
3
|
+
export declare type DocumentBaseMapType = {
|
|
4
|
+
[key: string]: DocumentBaseValueTypes;
|
|
5
|
+
};
|
|
6
|
+
export declare type DocumentBaseListType = DocumentBaseValueTypes[];
|
|
7
|
+
export declare type VersionedDocumentAuthor = {
|
|
8
|
+
type: 'user';
|
|
9
|
+
uuid: string;
|
|
10
|
+
name: string;
|
|
11
|
+
reason?: string;
|
|
12
|
+
} | {
|
|
13
|
+
type: 'system';
|
|
14
|
+
reason: string;
|
|
15
|
+
};
|
|
16
|
+
export declare type VersionedDocumentRequiredFields = {
|
|
17
|
+
timestamp: number;
|
|
18
|
+
author: VersionedDocumentAuthor;
|
|
19
|
+
};
|
|
20
|
+
export declare type TDocument<T> = {
|
|
21
|
+
[k in keyof T]: DocumentBaseValueTypes;
|
|
22
|
+
} & VersionedDocumentRequiredFields;
|
|
23
|
+
export declare type VersionedDocumentStoreCreator = typeof dynamoVersionedDocumentStore;
|