wirejs-deploy-amplify-basic 0.0.71-table-resource → 0.0.72-table-resource
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/amplify-hosting-assets/compute/default/package.json +1 -1
- package/dist/resources/distributed-table.js +52 -11
- package/package.json +4 -3
- package/dist/test/distributed-table.test.d.ts +0 -1
- package/dist/test/distributed-table.test.js +0 -28
- package/dist/wirejs-resources-overrides/client/index.d.ts +0 -1
- package/dist/wirejs-resources-overrides/client/index.js +0 -1
- package/dist/wirejs-resources-overrides/index.d.ts +0 -4
- package/dist/wirejs-resources-overrides/index.js +0 -14
- package/dist/wirejs-resources-overrides/internal/index.d.ts +0 -1
- package/dist/wirejs-resources-overrides/internal/index.js +0 -1
- package/dist/wirejs-resources-overrides/resource-collector.d.ts +0 -1
- package/dist/wirejs-resources-overrides/resource-collector.js +0 -7
- package/dist/wirejs-resources-overrides/resources/distributed-table.d.ts +0 -41
- package/dist/wirejs-resources-overrides/resources/distributed-table.js +0 -267
- package/dist/wirejs-resources-overrides/services/authentication.d.ts +0 -19
- package/dist/wirejs-resources-overrides/services/authentication.js +0 -507
- package/dist/wirejs-resources-overrides/services/file.d.ts +0 -13
- package/dist/wirejs-resources-overrides/services/file.js +0 -63
|
@@ -3,7 +3,11 @@ import { DynamoDBClient, } from '@aws-sdk/client-dynamodb';
|
|
|
3
3
|
import { PutCommand, GetCommand, QueryCommand, ScanCommand, DeleteCommand, } from '@aws-sdk/lib-dynamodb';
|
|
4
4
|
import { Resource, indexName } from 'wirejs-resources';
|
|
5
5
|
import { addResource } from '../resource-collector.js';
|
|
6
|
+
function fieldAlias(name) {
|
|
7
|
+
return `#a_${name}`;
|
|
8
|
+
}
|
|
6
9
|
function isFieldComparison(filter) {
|
|
10
|
+
console.log('Checking if filter is a field comparison:', filter);
|
|
7
11
|
return !['and', 'or', 'not'].some(key => key in filter);
|
|
8
12
|
}
|
|
9
13
|
function buildFilterExpression(filter) {
|
|
@@ -20,24 +24,25 @@ function buildFilterExpression(filter) {
|
|
|
20
24
|
if (!isFieldComparison(filter)) {
|
|
21
25
|
throw new Error(`Unsupported filter: ${JSON.stringify(filter)}`);
|
|
22
26
|
}
|
|
23
|
-
const [
|
|
24
|
-
const
|
|
27
|
+
const [baseField] = Object.keys(filter);
|
|
28
|
+
const field = fieldAlias(baseField);
|
|
29
|
+
const condition = filter[baseField];
|
|
25
30
|
if ('eq' in condition)
|
|
26
|
-
return `${field} = :${
|
|
31
|
+
return `${field} = :${baseField}`;
|
|
27
32
|
if ('ne' in condition)
|
|
28
|
-
return `${field} <> :${
|
|
33
|
+
return `${field} <> :${baseField}`;
|
|
29
34
|
if ('gt' in condition)
|
|
30
|
-
return `${field} > :${
|
|
35
|
+
return `${field} > :${baseField}`;
|
|
31
36
|
if ('ge' in condition)
|
|
32
|
-
return `${field} >= :${
|
|
37
|
+
return `${field} >= :${baseField}`;
|
|
33
38
|
if ('lt' in condition)
|
|
34
|
-
return `${field} < :${
|
|
39
|
+
return `${field} < :${baseField}`;
|
|
35
40
|
if ('le' in condition)
|
|
36
|
-
return `${field} <= :${
|
|
41
|
+
return `${field} <= :${baseField}`;
|
|
37
42
|
if ('between' in condition)
|
|
38
|
-
return `${field} BETWEEN :${
|
|
43
|
+
return `${field} BETWEEN :${baseField}Low AND :${baseField}High`;
|
|
39
44
|
if ('beginsWith' in condition)
|
|
40
|
-
return `begins_with(${field}, :${
|
|
45
|
+
return `begins_with(${field}, :${baseField})`;
|
|
41
46
|
throw new Error(`Unsupported filter condition: ${JSON.stringify(condition)}`);
|
|
42
47
|
}
|
|
43
48
|
function buildExpressionAttributeValues(filter) {
|
|
@@ -78,6 +83,27 @@ function buildExpressionAttributeValues(filter) {
|
|
|
78
83
|
}
|
|
79
84
|
return values;
|
|
80
85
|
}
|
|
86
|
+
function buildFieldAliasMap(filter) {
|
|
87
|
+
const aliasMap = {};
|
|
88
|
+
if (filter.and) {
|
|
89
|
+
for (const subFilter of filter.and) {
|
|
90
|
+
Object.assign(aliasMap, buildFieldAliasMap(subFilter));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (filter.or) {
|
|
94
|
+
for (const subFilter of filter.or) {
|
|
95
|
+
Object.assign(aliasMap, buildFieldAliasMap(subFilter));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (filter.not) {
|
|
99
|
+
Object.assign(aliasMap, buildFieldAliasMap(filter.not));
|
|
100
|
+
}
|
|
101
|
+
else if (isFieldComparison(filter)) {
|
|
102
|
+
const [field] = Object.keys(filter);
|
|
103
|
+
aliasMap[fieldAlias(field)] = field;
|
|
104
|
+
}
|
|
105
|
+
return aliasMap;
|
|
106
|
+
}
|
|
81
107
|
/**
|
|
82
108
|
* A table of records that favors very high *overall* scalability at the expense of
|
|
83
109
|
* scalability *between* partitions. Providers will distribute your data across many
|
|
@@ -168,12 +194,14 @@ export class DistributedTable extends Resource {
|
|
|
168
194
|
console.log('Scanning DynamoDB table:', this.table, options);
|
|
169
195
|
const filterExpression = options.filter ? buildFilterExpression(options.filter) : undefined;
|
|
170
196
|
const expressionAttributeValues = options.filter ? buildExpressionAttributeValues(options.filter) : undefined;
|
|
197
|
+
const expressionAttributeNames = options.filter ? buildFieldAliasMap(options.filter) : undefined;
|
|
171
198
|
let lastEvaluatedKey = undefined;
|
|
172
199
|
do {
|
|
173
200
|
const result = await this.ddbClient.send(new ScanCommand({
|
|
174
201
|
TableName: this.table,
|
|
175
202
|
FilterExpression: filterExpression,
|
|
176
203
|
ExpressionAttributeValues: expressionAttributeValues,
|
|
204
|
+
ExpressionAttributeNames: expressionAttributeNames,
|
|
177
205
|
ExclusiveStartKey: lastEvaluatedKey,
|
|
178
206
|
}));
|
|
179
207
|
for (const item of result.Items || []) {
|
|
@@ -190,7 +218,7 @@ export class DistributedTable extends Resource {
|
|
|
190
218
|
// Build the key condition expression from the `where` clause
|
|
191
219
|
const whereClauseAsFilter = {
|
|
192
220
|
// decompose each `where` clause property into a separate condition.
|
|
193
|
-
and: Object.entries(options.where).map(([k, v]) =>
|
|
221
|
+
and: Object.entries(options.where).map(([k, v]) => ({ [k]: v }))
|
|
194
222
|
};
|
|
195
223
|
const keyConditionExpression = buildFilterExpression(whereClauseAsFilter);
|
|
196
224
|
// Build the filter expression if provided
|
|
@@ -200,10 +228,22 @@ export class DistributedTable extends Resource {
|
|
|
200
228
|
...buildExpressionAttributeValues(whereClauseAsFilter),
|
|
201
229
|
...(options.filter ? buildExpressionAttributeValues(options.filter) : {}),
|
|
202
230
|
};
|
|
231
|
+
const expressionAttributeNames = {
|
|
232
|
+
...(options.filter ? buildFieldAliasMap(options.filter) : {}),
|
|
233
|
+
...buildFieldAliasMap(whereClauseAsFilter),
|
|
234
|
+
};
|
|
203
235
|
const isIndexNameTheDefault = options.by === indexName({
|
|
204
236
|
partition: this.key.partition,
|
|
205
237
|
sort: this.key.sort
|
|
206
238
|
});
|
|
239
|
+
console.log('DynamoDB query params', {
|
|
240
|
+
TableName: this.table,
|
|
241
|
+
IndexName: isIndexNameTheDefault ? undefined : options.by,
|
|
242
|
+
KeyConditionExpression: keyConditionExpression,
|
|
243
|
+
FilterExpression: filterExpression,
|
|
244
|
+
ExpressionAttributeValues: expressionAttributeValues,
|
|
245
|
+
ExpressionAttributeNames: expressionAttributeNames,
|
|
246
|
+
});
|
|
207
247
|
let lastEvaluatedKey = undefined;
|
|
208
248
|
do {
|
|
209
249
|
const result = await this.ddbClient.send(new QueryCommand({
|
|
@@ -212,6 +252,7 @@ export class DistributedTable extends Resource {
|
|
|
212
252
|
KeyConditionExpression: keyConditionExpression,
|
|
213
253
|
FilterExpression: filterExpression,
|
|
214
254
|
ExpressionAttributeValues: expressionAttributeValues,
|
|
255
|
+
ExpressionAttributeNames: expressionAttributeNames,
|
|
215
256
|
ExclusiveStartKey: lastEvaluatedKey,
|
|
216
257
|
}));
|
|
217
258
|
for (const item of result.Items || []) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wirejs-deploy-amplify-basic",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.72-table-resource",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -32,14 +32,15 @@
|
|
|
32
32
|
"recursive-copy": "^2.0.14",
|
|
33
33
|
"rimraf": "^6.0.1",
|
|
34
34
|
"wirejs-dom": "^1.0.38",
|
|
35
|
-
"wirejs-resources": "^0.1.
|
|
35
|
+
"wirejs-resources": "^0.1.40-table-resource"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@aws-amplify/backend": "^1.14.0",
|
|
39
39
|
"typescript": "^5.7.3"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"
|
|
42
|
+
"clean": "rimraf dist",
|
|
43
|
+
"build": "npm run clean && tsc",
|
|
43
44
|
"test": "node --import tsx --test test/**/*.test.ts"
|
|
44
45
|
},
|
|
45
46
|
"files": [
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { DistributedTable } from '../wirejs-resources-overrides/resources/distributed-table';
|
|
2
|
-
import { describe, it } from 'node:test';
|
|
3
|
-
describe('DistributedTable', () => {
|
|
4
|
-
it('should create a DistributedTable instance', async () => {
|
|
5
|
-
const table = new DistributedTable('app', 'users', {
|
|
6
|
-
parse: (record) => record,
|
|
7
|
-
key: {
|
|
8
|
-
partition: { field: 'id', type: 'string' },
|
|
9
|
-
sort: { field: 'name', type: 'string' }
|
|
10
|
-
},
|
|
11
|
-
indexes: [
|
|
12
|
-
{
|
|
13
|
-
partition: { field: 'age', type: 'number' },
|
|
14
|
-
sort: { field: 'city', type: 'string' }
|
|
15
|
-
}
|
|
16
|
-
]
|
|
17
|
-
});
|
|
18
|
-
await Array.fromAsync(table.query({
|
|
19
|
-
by: 'id-name',
|
|
20
|
-
where: {
|
|
21
|
-
id: { eq: '123' },
|
|
22
|
-
name: { eq: 'John Doe' }
|
|
23
|
-
}
|
|
24
|
-
}));
|
|
25
|
-
console.log('something');
|
|
26
|
-
// throw new Error('Test not implemented');
|
|
27
|
-
});
|
|
28
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from 'wirejs-resources/client';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from 'wirejs-resources/client';
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { overrides } from 'wirejs-resources';
|
|
2
|
-
// let's try exporting all the things and overwriting the specific things we
|
|
3
|
-
// want to re-implement.
|
|
4
|
-
export * from 'wirejs-resources';
|
|
5
|
-
import { FileService } from './services/file.js';
|
|
6
|
-
export { FileService } from './services/file.js';
|
|
7
|
-
import { AuthenticationService } from './services/authentication.js';
|
|
8
|
-
export { AuthenticationService } from './services/authentication.js';
|
|
9
|
-
import { DistributedTable } from './resources/distributed-table.js';
|
|
10
|
-
export { DistributedTable } from './resources/distributed-table.js';
|
|
11
|
-
// expose resources to other resources that might depend on it.
|
|
12
|
-
overrides.AuthenticationService = AuthenticationService;
|
|
13
|
-
overrides.DistributedTable = DistributedTable;
|
|
14
|
-
overrides.FileService = FileService;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from 'wirejs-resources/internal';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from 'wirejs-resources/internal';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function addResource(type: string, options: any): void;
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
2
|
-
import { AllIndexesByName, KeyCondition, IndexFieldNames, Filter, Index, KindaPretty, Parser, RecordKey, Resource, DistributedTable as BaseDistributedTable } from 'wirejs-resources';
|
|
3
|
-
/**
|
|
4
|
-
* A table of records that favors very high *overall* scalability at the expense of
|
|
5
|
-
* scalability *between* partitions. Providers will distribute your data across many
|
|
6
|
-
* servers based on the partition key as the table and/or traffic increases.
|
|
7
|
-
*
|
|
8
|
-
* ### Do NOT change partition keys. (In Production.)
|
|
9
|
-
*
|
|
10
|
-
* Changing it will cause some providers to drop and recreate your table.
|
|
11
|
-
*
|
|
12
|
-
* High cardinality, non-sequential partition keys allow for the best overall scaling.
|
|
13
|
-
*/
|
|
14
|
-
export declare class DistributedTable<const P extends Parser<any>, const T extends KindaPretty<ReturnType<P>>, const Key extends Index<T>, const Indexes extends Index<T>[] | undefined = undefined> extends Resource implements Omit<BaseDistributedTable<P, T, Key, Indexes>, '#private'> {
|
|
15
|
-
#private;
|
|
16
|
-
parse: P;
|
|
17
|
-
key: Key;
|
|
18
|
-
indexes: Indexes | undefined;
|
|
19
|
-
ddbClient: DynamoDBClient;
|
|
20
|
-
private table;
|
|
21
|
-
constructor(scope: Resource | string, id: string, options: {
|
|
22
|
-
parse: P;
|
|
23
|
-
key: Key;
|
|
24
|
-
indexes?: Indexes;
|
|
25
|
-
});
|
|
26
|
-
get partitionKeyName(): Key['partition']['field'];
|
|
27
|
-
get sortKeyName(): 'field' extends keyof Key['sort'] ? (Key['sort']['field'] extends string ? Key['sort']['field'] : undefined) : undefined;
|
|
28
|
-
save(item: T): Promise<void>;
|
|
29
|
-
saveMany(items: T[]): Promise<void>;
|
|
30
|
-
delete(item: RecordKey<T, Key>): Promise<void>;
|
|
31
|
-
deleteMany(items: (RecordKey<T, Key>)[]): Promise<void>;
|
|
32
|
-
get(key: RecordKey<T, Key>): Promise<T | undefined>;
|
|
33
|
-
scan(options?: {
|
|
34
|
-
filter?: Filter<T>;
|
|
35
|
-
}): AsyncGenerator<T>;
|
|
36
|
-
query<const GivenPartition extends keyof AllIndexesByName<BaseDistributedTable<P, T, Key, Indexes>> & string>(options: {
|
|
37
|
-
by: GivenPartition;
|
|
38
|
-
where: KeyCondition<BaseDistributedTable<P, T, Key, Indexes>, GivenPartition>;
|
|
39
|
-
filter?: Filter<Omit<T, IndexFieldNames<BaseDistributedTable<P, T, Key, Indexes>, GivenPartition> & string>>;
|
|
40
|
-
}): AsyncGenerator<T>;
|
|
41
|
-
}
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import { env } from 'process';
|
|
2
|
-
import { DynamoDBClient, } from '@aws-sdk/client-dynamodb';
|
|
3
|
-
import { PutCommand, GetCommand, QueryCommand, ScanCommand, DeleteCommand, } from '@aws-sdk/lib-dynamodb';
|
|
4
|
-
import { Resource, indexName } from 'wirejs-resources';
|
|
5
|
-
import { addResource } from '../resource-collector.js';
|
|
6
|
-
function fieldAlias(name) {
|
|
7
|
-
return `#a_${name}`;
|
|
8
|
-
}
|
|
9
|
-
function isFieldComparison(filter) {
|
|
10
|
-
console.log('Checking if filter is a field comparison:', filter);
|
|
11
|
-
return !['and', 'or', 'not'].some(key => key in filter);
|
|
12
|
-
}
|
|
13
|
-
function buildFilterExpression(filter) {
|
|
14
|
-
console.log('Building filter expression for filter:', filter);
|
|
15
|
-
if (filter.and) {
|
|
16
|
-
return `(${filter.and.map(buildFilterExpression).join(' AND ')})`;
|
|
17
|
-
}
|
|
18
|
-
if (filter.or) {
|
|
19
|
-
return `(${filter.or.map(buildFilterExpression).join(' OR ')})`;
|
|
20
|
-
}
|
|
21
|
-
if (filter.not) {
|
|
22
|
-
return `(NOT ${buildFilterExpression(filter.not)})`;
|
|
23
|
-
}
|
|
24
|
-
if (!isFieldComparison(filter)) {
|
|
25
|
-
throw new Error(`Unsupported filter: ${JSON.stringify(filter)}`);
|
|
26
|
-
}
|
|
27
|
-
const [baseField] = Object.keys(filter);
|
|
28
|
-
const field = fieldAlias(baseField);
|
|
29
|
-
const condition = filter[baseField];
|
|
30
|
-
if ('eq' in condition)
|
|
31
|
-
return `${field} = :${baseField}`;
|
|
32
|
-
if ('ne' in condition)
|
|
33
|
-
return `${field} <> :${baseField}`;
|
|
34
|
-
if ('gt' in condition)
|
|
35
|
-
return `${field} > :${baseField}`;
|
|
36
|
-
if ('ge' in condition)
|
|
37
|
-
return `${field} >= :${baseField}`;
|
|
38
|
-
if ('lt' in condition)
|
|
39
|
-
return `${field} < :${baseField}`;
|
|
40
|
-
if ('le' in condition)
|
|
41
|
-
return `${field} <= :${baseField}`;
|
|
42
|
-
if ('between' in condition)
|
|
43
|
-
return `${field} BETWEEN :${baseField}Low AND :${baseField}High`;
|
|
44
|
-
if ('beginsWith' in condition)
|
|
45
|
-
return `begins_with(${field}, :${baseField})`;
|
|
46
|
-
throw new Error(`Unsupported filter condition: ${JSON.stringify(condition)}`);
|
|
47
|
-
}
|
|
48
|
-
function buildExpressionAttributeValues(filter) {
|
|
49
|
-
console.log('Building expression attribute values for filter:', filter);
|
|
50
|
-
const values = {};
|
|
51
|
-
if (filter.and || filter.or || filter.not) {
|
|
52
|
-
const subFilters = filter.and ?? filter.or ?? [filter.not];
|
|
53
|
-
for (const subFilter of subFilters) {
|
|
54
|
-
if (!subFilter)
|
|
55
|
-
continue;
|
|
56
|
-
Object.assign(values, buildExpressionAttributeValues(subFilter));
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
else if (isFieldComparison(filter)) {
|
|
60
|
-
const [field] = Object.keys(filter);
|
|
61
|
-
const condition = filter[field];
|
|
62
|
-
if ('eq' in condition)
|
|
63
|
-
values[`:${field}`] = { S: condition.eq };
|
|
64
|
-
if ('ne' in condition)
|
|
65
|
-
values[`:${field}`] = { S: condition.ne };
|
|
66
|
-
if ('gt' in condition)
|
|
67
|
-
values[`:${field}`] = { S: condition.gt };
|
|
68
|
-
if ('ge' in condition)
|
|
69
|
-
values[`:${field}`] = { S: condition.ge };
|
|
70
|
-
if ('lt' in condition)
|
|
71
|
-
values[`:${field}`] = { S: condition.lt };
|
|
72
|
-
if ('le' in condition)
|
|
73
|
-
values[`:${field}`] = { S: condition.le };
|
|
74
|
-
if ('between' in condition) {
|
|
75
|
-
values[`:${field}Low`] = { S: condition.between[0] };
|
|
76
|
-
values[`:${field}High`] = { S: condition.between[1] };
|
|
77
|
-
}
|
|
78
|
-
if ('beginsWith' in condition)
|
|
79
|
-
values[`:${field}`] = { S: condition.beginsWith };
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
throw new Error(`Unsupported filter: ${JSON.stringify(filter)}`);
|
|
83
|
-
}
|
|
84
|
-
return values;
|
|
85
|
-
}
|
|
86
|
-
function buildFieldAliasMap(filter) {
|
|
87
|
-
const aliasMap = {};
|
|
88
|
-
if (filter.and) {
|
|
89
|
-
for (const subFilter of filter.and) {
|
|
90
|
-
Object.assign(aliasMap, buildFieldAliasMap(subFilter));
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
else if (filter.or) {
|
|
94
|
-
for (const subFilter of filter.or) {
|
|
95
|
-
Object.assign(aliasMap, buildFieldAliasMap(subFilter));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
else if (filter.not) {
|
|
99
|
-
Object.assign(aliasMap, buildFieldAliasMap(filter.not));
|
|
100
|
-
}
|
|
101
|
-
else if (isFieldComparison(filter)) {
|
|
102
|
-
const [field] = Object.keys(filter);
|
|
103
|
-
aliasMap[fieldAlias(field)] = field;
|
|
104
|
-
}
|
|
105
|
-
return aliasMap;
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* A table of records that favors very high *overall* scalability at the expense of
|
|
109
|
-
* scalability *between* partitions. Providers will distribute your data across many
|
|
110
|
-
* servers based on the partition key as the table and/or traffic increases.
|
|
111
|
-
*
|
|
112
|
-
* ### Do NOT change partition keys. (In Production.)
|
|
113
|
-
*
|
|
114
|
-
* Changing it will cause some providers to drop and recreate your table.
|
|
115
|
-
*
|
|
116
|
-
* High cardinality, non-sequential partition keys allow for the best overall scaling.
|
|
117
|
-
*/
|
|
118
|
-
export class DistributedTable extends Resource {
|
|
119
|
-
parse;
|
|
120
|
-
key;
|
|
121
|
-
indexes;
|
|
122
|
-
ddbClient;
|
|
123
|
-
table;
|
|
124
|
-
constructor(scope, id, options) {
|
|
125
|
-
super(scope, id);
|
|
126
|
-
this.parse = options.parse;
|
|
127
|
-
this.key = options.key;
|
|
128
|
-
this.indexes = options.indexes;
|
|
129
|
-
this.ddbClient = new DynamoDBClient();
|
|
130
|
-
this.table = env['TABLE_NAME_PREFIX'] + this.absoluteId.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
131
|
-
const resourceDefinition = {
|
|
132
|
-
absoluteId: this.absoluteId,
|
|
133
|
-
partitionKey: this.key.partition,
|
|
134
|
-
sortKey: this.key.sort,
|
|
135
|
-
indexes: this.indexes
|
|
136
|
-
};
|
|
137
|
-
addResource('DistributedTable', resourceDefinition);
|
|
138
|
-
}
|
|
139
|
-
get partitionKeyName() {
|
|
140
|
-
return this.key.partition.field;
|
|
141
|
-
}
|
|
142
|
-
get sortKeyName() {
|
|
143
|
-
return this.key.sort?.field;
|
|
144
|
-
}
|
|
145
|
-
#getDDBKey(key) {
|
|
146
|
-
const ddbKey = {
|
|
147
|
-
[this.partitionKeyName]: key[this.partitionKeyName]
|
|
148
|
-
};
|
|
149
|
-
if (typeof this.sortKeyName === 'string') {
|
|
150
|
-
ddbKey[this.sortKeyName] = key[this.sortKeyName];
|
|
151
|
-
}
|
|
152
|
-
return ddbKey;
|
|
153
|
-
}
|
|
154
|
-
async save(item) {
|
|
155
|
-
const key = this.#getDDBKey(item);
|
|
156
|
-
const itemToSave = {
|
|
157
|
-
...key,
|
|
158
|
-
...item,
|
|
159
|
-
};
|
|
160
|
-
console.log('Saving item to DynamoDB:', itemToSave);
|
|
161
|
-
await this.ddbClient.send(new PutCommand({
|
|
162
|
-
TableName: this.table,
|
|
163
|
-
Item: itemToSave,
|
|
164
|
-
}));
|
|
165
|
-
}
|
|
166
|
-
async saveMany(items) {
|
|
167
|
-
const promises = items.map(item => this.save(item));
|
|
168
|
-
await Promise.all(promises);
|
|
169
|
-
}
|
|
170
|
-
async delete(item) {
|
|
171
|
-
const key = this.#getDDBKey(item);
|
|
172
|
-
console.log('Deleting item from DynamoDB:', key);
|
|
173
|
-
await this.ddbClient.send(new DeleteCommand({
|
|
174
|
-
TableName: this.table,
|
|
175
|
-
Key: key,
|
|
176
|
-
}));
|
|
177
|
-
}
|
|
178
|
-
async deleteMany(items) {
|
|
179
|
-
const promises = items.map(item => this.delete(item));
|
|
180
|
-
await Promise.all(promises);
|
|
181
|
-
}
|
|
182
|
-
async get(key) {
|
|
183
|
-
const ddbKey = this.#getDDBKey(key);
|
|
184
|
-
console.log('Getting item from DynamoDB:', ddbKey);
|
|
185
|
-
const result = await this.ddbClient.send(new GetCommand({
|
|
186
|
-
TableName: this.table,
|
|
187
|
-
Key: ddbKey,
|
|
188
|
-
}));
|
|
189
|
-
if (!result.Item)
|
|
190
|
-
return undefined;
|
|
191
|
-
return this.parse(result.Item);
|
|
192
|
-
}
|
|
193
|
-
async *scan(options = {}) {
|
|
194
|
-
console.log('Scanning DynamoDB table:', this.table, options);
|
|
195
|
-
const filterExpression = options.filter ? buildFilterExpression(options.filter) : undefined;
|
|
196
|
-
const expressionAttributeValues = options.filter ? buildExpressionAttributeValues(options.filter) : undefined;
|
|
197
|
-
const expressionAttributeNames = options.filter ? buildFieldAliasMap(options.filter) : undefined;
|
|
198
|
-
let lastEvaluatedKey = undefined;
|
|
199
|
-
do {
|
|
200
|
-
const result = await this.ddbClient.send(new ScanCommand({
|
|
201
|
-
TableName: this.table,
|
|
202
|
-
FilterExpression: filterExpression,
|
|
203
|
-
ExpressionAttributeValues: expressionAttributeValues,
|
|
204
|
-
ExpressionAttributeNames: expressionAttributeNames,
|
|
205
|
-
ExclusiveStartKey: lastEvaluatedKey,
|
|
206
|
-
}));
|
|
207
|
-
for (const item of result.Items || []) {
|
|
208
|
-
if (!item)
|
|
209
|
-
continue;
|
|
210
|
-
const record = this.parse(item);
|
|
211
|
-
yield record;
|
|
212
|
-
}
|
|
213
|
-
lastEvaluatedKey = result.LastEvaluatedKey;
|
|
214
|
-
} while (lastEvaluatedKey);
|
|
215
|
-
}
|
|
216
|
-
async *query(options) {
|
|
217
|
-
console.log('Querying DynamoDB table:', this.table, options);
|
|
218
|
-
// Build the key condition expression from the `where` clause
|
|
219
|
-
const whereClauseAsFilter = {
|
|
220
|
-
// decompose each `where` clause property into a separate condition.
|
|
221
|
-
and: Object.entries(options.where).map(([k, v]) => ({ [k]: v }))
|
|
222
|
-
};
|
|
223
|
-
const keyConditionExpression = buildFilterExpression(whereClauseAsFilter);
|
|
224
|
-
// Build the filter expression if provided
|
|
225
|
-
const filterExpression = options.filter ? buildFilterExpression(options.filter) : undefined;
|
|
226
|
-
// Combine expression attribute values for both key conditions and filters
|
|
227
|
-
const expressionAttributeValues = {
|
|
228
|
-
...buildExpressionAttributeValues(whereClauseAsFilter),
|
|
229
|
-
...(options.filter ? buildExpressionAttributeValues(options.filter) : {}),
|
|
230
|
-
};
|
|
231
|
-
const expressionAttributeNames = {
|
|
232
|
-
...(options.filter ? buildFieldAliasMap(options.filter) : {}),
|
|
233
|
-
...buildFieldAliasMap(whereClauseAsFilter),
|
|
234
|
-
};
|
|
235
|
-
const isIndexNameTheDefault = options.by === indexName({
|
|
236
|
-
partition: this.key.partition,
|
|
237
|
-
sort: this.key.sort
|
|
238
|
-
});
|
|
239
|
-
console.log('DynamoDB query params', {
|
|
240
|
-
TableName: this.table,
|
|
241
|
-
IndexName: isIndexNameTheDefault ? undefined : options.by,
|
|
242
|
-
KeyConditionExpression: keyConditionExpression,
|
|
243
|
-
FilterExpression: filterExpression,
|
|
244
|
-
ExpressionAttributeValues: expressionAttributeValues,
|
|
245
|
-
ExpressionAttributeNames: expressionAttributeNames,
|
|
246
|
-
});
|
|
247
|
-
let lastEvaluatedKey = undefined;
|
|
248
|
-
do {
|
|
249
|
-
const result = await this.ddbClient.send(new QueryCommand({
|
|
250
|
-
TableName: this.table,
|
|
251
|
-
IndexName: isIndexNameTheDefault ? undefined : options.by,
|
|
252
|
-
KeyConditionExpression: keyConditionExpression,
|
|
253
|
-
FilterExpression: filterExpression,
|
|
254
|
-
ExpressionAttributeValues: expressionAttributeValues,
|
|
255
|
-
ExpressionAttributeNames: expressionAttributeNames,
|
|
256
|
-
ExclusiveStartKey: lastEvaluatedKey,
|
|
257
|
-
}));
|
|
258
|
-
for (const item of result.Items || []) {
|
|
259
|
-
if (!item)
|
|
260
|
-
continue;
|
|
261
|
-
const record = this.parse(item);
|
|
262
|
-
yield record;
|
|
263
|
-
}
|
|
264
|
-
lastEvaluatedKey = result.LastEvaluatedKey;
|
|
265
|
-
} while (lastEvaluatedKey);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Resource, ContextWrapped, CookieJar, AuthenticationError, AuthenticationService as AuthenticationServiceBase, AuthenticationMachineState, AuthenticationServiceOptions, AuthenticationMachineInput, User } from 'wirejs-resources';
|
|
2
|
-
export declare function hasNonEmptyString(o: any, k: string): boolean;
|
|
3
|
-
export declare class AuthenticationService extends AuthenticationServiceBase {
|
|
4
|
-
#private;
|
|
5
|
-
constructor(scope: Resource | string, id: string, options?: AuthenticationServiceOptions);
|
|
6
|
-
getMachineState(cookies: CookieJar): Promise<AuthenticationMachineState>;
|
|
7
|
-
missingFieldErrors<T extends Record<string, string | number | boolean>>(input: T, fields: (keyof T & string)[]): AuthenticationError[] | undefined;
|
|
8
|
-
setMachineState(cookies: CookieJar, form: AuthenticationMachineInput): Promise<AuthenticationMachineState | {
|
|
9
|
-
errors: AuthenticationError[];
|
|
10
|
-
}>;
|
|
11
|
-
buildApi(): ContextWrapped<{
|
|
12
|
-
getState: () => Promise<AuthenticationMachineState>;
|
|
13
|
-
setState: (options: AuthenticationMachineInput) => Promise<AuthenticationMachineState | {
|
|
14
|
-
errors: AuthenticationError[];
|
|
15
|
-
}>;
|
|
16
|
-
getCurrentUser: () => Promise<User | null>;
|
|
17
|
-
requireCurrentUser: () => Promise<User>;
|
|
18
|
-
}>;
|
|
19
|
-
}
|
|
@@ -1,507 +0,0 @@
|
|
|
1
|
-
import { env } from 'process';
|
|
2
|
-
import * as jose from 'jose';
|
|
3
|
-
import { CognitoIdentityProviderClient, SignUpCommand, ForgotPasswordCommand, ConfirmForgotPasswordCommand, ConfirmSignUpCommand, ResendConfirmationCodeCommand, InitiateAuthCommand, ChangePasswordCommand, } from '@aws-sdk/client-cognito-identity-provider';
|
|
4
|
-
import { withContext, SignedCookie, Secret, AuthenticationService as AuthenticationServiceBase, } from 'wirejs-resources';
|
|
5
|
-
import { addResource } from '../resource-collector.js';
|
|
6
|
-
const ClientId = env['COGNITO_CLIENT_ID'];
|
|
7
|
-
const actions = {
|
|
8
|
-
changepassword: {
|
|
9
|
-
name: "Change Password",
|
|
10
|
-
fields: {
|
|
11
|
-
existingPassword: {
|
|
12
|
-
label: 'Old Password',
|
|
13
|
-
type: 'password',
|
|
14
|
-
},
|
|
15
|
-
newPassword: {
|
|
16
|
-
label: 'New Password',
|
|
17
|
-
type: 'password',
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
buttons: ['Change Password']
|
|
21
|
-
},
|
|
22
|
-
signin: {
|
|
23
|
-
name: "Sign In",
|
|
24
|
-
fields: {
|
|
25
|
-
email: {
|
|
26
|
-
label: 'Email',
|
|
27
|
-
type: 'text',
|
|
28
|
-
},
|
|
29
|
-
password: {
|
|
30
|
-
label: 'Password',
|
|
31
|
-
type: 'password',
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
buttons: ['Sign In']
|
|
35
|
-
},
|
|
36
|
-
startforgotpassword: {
|
|
37
|
-
name: "Forgot Password"
|
|
38
|
-
},
|
|
39
|
-
continueforgotpassword: {
|
|
40
|
-
name: "Forgot Password",
|
|
41
|
-
fields: {
|
|
42
|
-
email: {
|
|
43
|
-
label: "Email",
|
|
44
|
-
type: "text"
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
buttons: ["Send Reset Code"]
|
|
48
|
-
},
|
|
49
|
-
completeforgotpassword: {
|
|
50
|
-
name: "Reset Password",
|
|
51
|
-
fields: {
|
|
52
|
-
code: {
|
|
53
|
-
label: "Reset Code",
|
|
54
|
-
type: "text"
|
|
55
|
-
},
|
|
56
|
-
password: {
|
|
57
|
-
label: "New Password",
|
|
58
|
-
type: 'password'
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
buttons: ["Set Password"]
|
|
62
|
-
},
|
|
63
|
-
startsignup: {
|
|
64
|
-
name: "Sign Up",
|
|
65
|
-
fields: {
|
|
66
|
-
email: {
|
|
67
|
-
label: 'Email',
|
|
68
|
-
type: 'text',
|
|
69
|
-
},
|
|
70
|
-
password: {
|
|
71
|
-
label: 'Password',
|
|
72
|
-
type: 'password',
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
buttons: ['Sign Up']
|
|
76
|
-
},
|
|
77
|
-
completesignup: {
|
|
78
|
-
name: "Finish Signing Up",
|
|
79
|
-
fields: {
|
|
80
|
-
code: {
|
|
81
|
-
label: "Confirmation Code",
|
|
82
|
-
type: 'text'
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
buttons: ['Complete Sign-up']
|
|
86
|
-
},
|
|
87
|
-
resendsignupcode: {
|
|
88
|
-
name: "Resend Confirmation Code"
|
|
89
|
-
},
|
|
90
|
-
signout: {
|
|
91
|
-
name: "Sign out"
|
|
92
|
-
},
|
|
93
|
-
cancel: {
|
|
94
|
-
name: "Cancel"
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
function machineAction(key) {
|
|
98
|
-
return {
|
|
99
|
-
key,
|
|
100
|
-
...actions[key]
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
function machineActions(...keys) {
|
|
104
|
-
const result = {};
|
|
105
|
-
for (const k of keys) {
|
|
106
|
-
result[k] = machineAction(k);
|
|
107
|
-
}
|
|
108
|
-
return result;
|
|
109
|
-
}
|
|
110
|
-
function isAction(input, action) {
|
|
111
|
-
return input.key === action;
|
|
112
|
-
}
|
|
113
|
-
export function hasNonEmptyString(o, k) {
|
|
114
|
-
return (typeof o === 'object' && k in o && typeof o[k] === 'string' && o[k].length > 0);
|
|
115
|
-
}
|
|
116
|
-
const ONE_WEEK = 7 * 24 * 60 * 60; // days * hours/day * minutes/hour * seconds/minute
|
|
117
|
-
const client = new CognitoIdentityProviderClient();
|
|
118
|
-
export class AuthenticationService extends AuthenticationServiceBase {
|
|
119
|
-
#cookie;
|
|
120
|
-
#keepalive;
|
|
121
|
-
constructor(scope, id, options = {}) {
|
|
122
|
-
super(scope, id, options);
|
|
123
|
-
const signingSecret = new Secret(this, 'jwt-signing-secret');
|
|
124
|
-
this.#keepalive = options.keepalive ?? false;
|
|
125
|
-
this.#cookie = new SignedCookie(this, options.cookie ?? 'identity', signingSecret, { maxAge: ONE_WEEK });
|
|
126
|
-
addResource('AuthenticationService', { absoluteId: this.absoluteId });
|
|
127
|
-
}
|
|
128
|
-
async getMachineState(cookies) {
|
|
129
|
-
const state = await this.#cookie.read(cookies);
|
|
130
|
-
if (state?.state === 'authenticated') {
|
|
131
|
-
if (this.#keepalive)
|
|
132
|
-
await this.#cookie.write(cookies, state);
|
|
133
|
-
return {
|
|
134
|
-
...state,
|
|
135
|
-
actions: machineActions('changepassword', 'signout')
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
if (state?.substate === 'pending-completesignup') {
|
|
140
|
-
return {
|
|
141
|
-
state: 'unauthenticated',
|
|
142
|
-
user: undefined,
|
|
143
|
-
actions: machineActions('completesignup', 'resendsignupcode', 'cancel')
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
else if (state?.substate === 'pending-continueforgotpassword') {
|
|
147
|
-
return {
|
|
148
|
-
state: 'unauthenticated',
|
|
149
|
-
user: undefined,
|
|
150
|
-
actions: machineActions('continueforgotpassword', 'cancel')
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
else if (state?.substate === 'pending-completeforgotpassword') {
|
|
154
|
-
return {
|
|
155
|
-
state: 'unauthenticated',
|
|
156
|
-
user: undefined,
|
|
157
|
-
actions: machineActions('completeforgotpassword', 'cancel')
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
return {
|
|
162
|
-
state: 'unauthenticated',
|
|
163
|
-
user: undefined,
|
|
164
|
-
actions: machineActions('signin', 'startsignup', 'startforgotpassword')
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
missingFieldErrors(input, fields) {
|
|
170
|
-
const errors = [];
|
|
171
|
-
for (const field of fields) {
|
|
172
|
-
if (!input[field])
|
|
173
|
-
errors.push({
|
|
174
|
-
field,
|
|
175
|
-
message: "Field is required."
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
return errors.length > 0 ? errors : undefined;
|
|
179
|
-
}
|
|
180
|
-
async setMachineState(cookies, form) {
|
|
181
|
-
if (isAction(form, 'signout')) {
|
|
182
|
-
this.#cookie.clear(cookies);
|
|
183
|
-
return this.getMachineState(cookies);
|
|
184
|
-
}
|
|
185
|
-
else if (isAction(form, 'cancel')) {
|
|
186
|
-
const state = await this.#cookie.read(cookies);
|
|
187
|
-
if (!state)
|
|
188
|
-
return this.getMachineState(cookies);
|
|
189
|
-
await this.#cookie.write(cookies, {
|
|
190
|
-
...state,
|
|
191
|
-
substate: undefined,
|
|
192
|
-
metadata: undefined
|
|
193
|
-
});
|
|
194
|
-
return this.getMachineState(cookies);
|
|
195
|
-
}
|
|
196
|
-
else if (isAction(form, 'startsignup')) {
|
|
197
|
-
const errors = this.missingFieldErrors(form.inputs, ['email', 'password']);
|
|
198
|
-
if (errors) {
|
|
199
|
-
return { errors };
|
|
200
|
-
}
|
|
201
|
-
try {
|
|
202
|
-
const command = new SignUpCommand({
|
|
203
|
-
ClientId,
|
|
204
|
-
Username: form.inputs.email,
|
|
205
|
-
Password: form.inputs.password,
|
|
206
|
-
UserAttributes: [
|
|
207
|
-
{ Name: 'email', Value: form.inputs.email }
|
|
208
|
-
]
|
|
209
|
-
});
|
|
210
|
-
const result = await client.send(command);
|
|
211
|
-
await this.#cookie.write(cookies, {
|
|
212
|
-
state: 'unauthenticated',
|
|
213
|
-
user: undefined,
|
|
214
|
-
substate: 'pending-completesignup',
|
|
215
|
-
metadata: form.inputs.email
|
|
216
|
-
});
|
|
217
|
-
return this.getMachineState(cookies);
|
|
218
|
-
}
|
|
219
|
-
catch (error) {
|
|
220
|
-
return {
|
|
221
|
-
errors: [{
|
|
222
|
-
message: error.message
|
|
223
|
-
}]
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
else if (isAction(form, 'resendsignupcode')) {
|
|
228
|
-
const state = await this.#cookie.read(cookies);
|
|
229
|
-
if (!state) {
|
|
230
|
-
this.#cookie.clear(cookies);
|
|
231
|
-
return this.getMachineState(cookies);
|
|
232
|
-
}
|
|
233
|
-
// bad state ...
|
|
234
|
-
if (state?.state === 'authenticated'
|
|
235
|
-
|| state?.substate !== 'pending-completesignup'
|
|
236
|
-
|| !state?.metadata) {
|
|
237
|
-
await this.#cookie.write(cookies, {
|
|
238
|
-
...state,
|
|
239
|
-
substate: undefined,
|
|
240
|
-
metadata: undefined,
|
|
241
|
-
});
|
|
242
|
-
return this.getMachineState(cookies);
|
|
243
|
-
}
|
|
244
|
-
try {
|
|
245
|
-
const command = new ResendConfirmationCodeCommand({
|
|
246
|
-
ClientId,
|
|
247
|
-
Username: state.metadata
|
|
248
|
-
});
|
|
249
|
-
await client.send(command);
|
|
250
|
-
return {
|
|
251
|
-
...await this.getMachineState(cookies),
|
|
252
|
-
message: 'Your code has been sent again.'
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
catch (error) {
|
|
256
|
-
return {
|
|
257
|
-
errors: [{
|
|
258
|
-
message: error.message
|
|
259
|
-
}]
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
else if (isAction(form, 'completesignup')) {
|
|
264
|
-
const errors = this.missingFieldErrors(form.inputs, ['code']);
|
|
265
|
-
if (errors) {
|
|
266
|
-
return { errors };
|
|
267
|
-
}
|
|
268
|
-
const state = await this.#cookie.read(cookies);
|
|
269
|
-
if (!state || state.substate !== 'pending-completesignup') {
|
|
270
|
-
this.#cookie.clear(cookies);
|
|
271
|
-
return this.getMachineState(cookies);
|
|
272
|
-
}
|
|
273
|
-
try {
|
|
274
|
-
const email = state.metadata;
|
|
275
|
-
if (!email) {
|
|
276
|
-
this.#cookie.clear(cookies);
|
|
277
|
-
return this.getMachineState(cookies);
|
|
278
|
-
}
|
|
279
|
-
const command = new ConfirmSignUpCommand({
|
|
280
|
-
ClientId,
|
|
281
|
-
Username: email,
|
|
282
|
-
ConfirmationCode: form.inputs.code,
|
|
283
|
-
});
|
|
284
|
-
const result = await client.send(command);
|
|
285
|
-
await this.#cookie.clear(cookies);
|
|
286
|
-
return {
|
|
287
|
-
...await this.getMachineState(cookies),
|
|
288
|
-
message: 'All done setting up!<br />Give it a try by signing in.'
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
catch (error) {
|
|
292
|
-
return {
|
|
293
|
-
errors: [{
|
|
294
|
-
message: error.message
|
|
295
|
-
}]
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
else if (isAction(form, 'signin')) {
|
|
300
|
-
const errors = this.missingFieldErrors(form.inputs, ['email', 'password']);
|
|
301
|
-
if (errors) {
|
|
302
|
-
return { errors };
|
|
303
|
-
}
|
|
304
|
-
try {
|
|
305
|
-
const command = new InitiateAuthCommand({
|
|
306
|
-
ClientId,
|
|
307
|
-
AuthFlow: 'USER_PASSWORD_AUTH',
|
|
308
|
-
AuthParameters: {
|
|
309
|
-
USERNAME: form.inputs.email,
|
|
310
|
-
PASSWORD: form.inputs.password,
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
const result = await client.send(command);
|
|
314
|
-
const jwtPayload = jose.decodeJwt(
|
|
315
|
-
// assuming for now, until we support challenges like OTP, Email code, etc.
|
|
316
|
-
result.AuthenticationResult?.IdToken);
|
|
317
|
-
await this.#cookie.write(cookies, {
|
|
318
|
-
state: 'authenticated',
|
|
319
|
-
user: {
|
|
320
|
-
id: jwtPayload.sub,
|
|
321
|
-
username: form.inputs.email,
|
|
322
|
-
displayName: form.inputs.email,
|
|
323
|
-
},
|
|
324
|
-
});
|
|
325
|
-
return this.getMachineState(cookies);
|
|
326
|
-
}
|
|
327
|
-
catch (error) {
|
|
328
|
-
if (error.message === 'User is not confirmed.') {
|
|
329
|
-
const command = new ResendConfirmationCodeCommand({
|
|
330
|
-
ClientId,
|
|
331
|
-
Username: form.inputs.email
|
|
332
|
-
});
|
|
333
|
-
await client.send(command);
|
|
334
|
-
await this.#cookie.write(cookies, {
|
|
335
|
-
state: 'unauthenticated',
|
|
336
|
-
user: undefined,
|
|
337
|
-
substate: 'pending-completesignup',
|
|
338
|
-
metadata: form.inputs.email,
|
|
339
|
-
});
|
|
340
|
-
return {
|
|
341
|
-
...await this.getMachineState(cookies),
|
|
342
|
-
message: `Your account setup isn't complete.<br />
|
|
343
|
-
We sent you a new code to enter below.`
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
return {
|
|
348
|
-
errors: [{
|
|
349
|
-
message: error.message
|
|
350
|
-
}]
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
else if (isAction(form, 'changepassword')) {
|
|
356
|
-
const errors = this.missingFieldErrors(form.inputs, ['existingPassword', 'newPassword']);
|
|
357
|
-
if (errors) {
|
|
358
|
-
return { errors };
|
|
359
|
-
}
|
|
360
|
-
const state = await this.#cookie.read(cookies);
|
|
361
|
-
if (state?.state !== 'authenticated') {
|
|
362
|
-
this.#cookie.clear(cookies);
|
|
363
|
-
return this.getMachineState(cookies);
|
|
364
|
-
}
|
|
365
|
-
try {
|
|
366
|
-
// change password requires an access token, which we don't actually store.
|
|
367
|
-
// so, first step is to actually authenticate.
|
|
368
|
-
const authCommand = new InitiateAuthCommand({
|
|
369
|
-
ClientId,
|
|
370
|
-
AuthFlow: 'USER_PASSWORD_AUTH',
|
|
371
|
-
AuthParameters: {
|
|
372
|
-
USERNAME: state.user.username,
|
|
373
|
-
PASSWORD: form.inputs.existingPassword,
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
const authResult = await client.send(authCommand);
|
|
377
|
-
const changePassCommand = new ChangePasswordCommand({
|
|
378
|
-
AccessToken: authResult.AuthenticationResult?.AccessToken,
|
|
379
|
-
PreviousPassword: form.inputs.existingPassword,
|
|
380
|
-
ProposedPassword: form.inputs.newPassword
|
|
381
|
-
});
|
|
382
|
-
await client.send(changePassCommand);
|
|
383
|
-
return {
|
|
384
|
-
...this.getMachineState(cookies),
|
|
385
|
-
message: 'Password changed.'
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
catch (error) {
|
|
389
|
-
return {
|
|
390
|
-
errors: [{
|
|
391
|
-
message: error.message
|
|
392
|
-
}]
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
else if (isAction(form, 'startforgotpassword')) {
|
|
397
|
-
const state = await this.#cookie.read(cookies);
|
|
398
|
-
if (state?.state === 'authenticated') {
|
|
399
|
-
// user is already signed in ... this is *probably* a rogue request?
|
|
400
|
-
return this.getMachineState(cookies);
|
|
401
|
-
}
|
|
402
|
-
await this.#cookie.write(cookies, {
|
|
403
|
-
state: 'unauthenticated',
|
|
404
|
-
user: undefined,
|
|
405
|
-
substate: 'pending-continueforgotpassword'
|
|
406
|
-
});
|
|
407
|
-
return this.getMachineState(cookies);
|
|
408
|
-
}
|
|
409
|
-
else if (isAction(form, 'continueforgotpassword')) {
|
|
410
|
-
const state = await this.#cookie.read(cookies);
|
|
411
|
-
if (state?.state === 'authenticated') {
|
|
412
|
-
// user is already signed in ... this is *probably* a rogue request?
|
|
413
|
-
return this.getMachineState(cookies);
|
|
414
|
-
}
|
|
415
|
-
const errors = this.missingFieldErrors(form.inputs, ['email']);
|
|
416
|
-
if (errors) {
|
|
417
|
-
return { errors };
|
|
418
|
-
}
|
|
419
|
-
try {
|
|
420
|
-
const command = new ForgotPasswordCommand({
|
|
421
|
-
ClientId,
|
|
422
|
-
Username: form.inputs.email
|
|
423
|
-
});
|
|
424
|
-
await client.send(command);
|
|
425
|
-
await this.#cookie.write(cookies, {
|
|
426
|
-
state: 'unauthenticated',
|
|
427
|
-
user: undefined,
|
|
428
|
-
substate: 'pending-completeforgotpassword',
|
|
429
|
-
metadata: form.inputs.email,
|
|
430
|
-
});
|
|
431
|
-
return this.getMachineState(cookies);
|
|
432
|
-
}
|
|
433
|
-
catch (error) {
|
|
434
|
-
return {
|
|
435
|
-
errors: [{
|
|
436
|
-
message: error.message
|
|
437
|
-
}]
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
else if (isAction(form, 'completeforgotpassword')) {
|
|
442
|
-
const state = await this.#cookie.read(cookies);
|
|
443
|
-
if (state?.state === 'authenticated') {
|
|
444
|
-
// user is already signed in ... this is *probably* a rogue request?
|
|
445
|
-
return this.getMachineState(cookies);
|
|
446
|
-
}
|
|
447
|
-
const errors = this.missingFieldErrors(form.inputs, ['code', 'password']);
|
|
448
|
-
if (errors) {
|
|
449
|
-
return { errors };
|
|
450
|
-
}
|
|
451
|
-
try {
|
|
452
|
-
const command = new ConfirmForgotPasswordCommand({
|
|
453
|
-
ClientId,
|
|
454
|
-
ConfirmationCode: form.inputs.code,
|
|
455
|
-
Password: form.inputs.password,
|
|
456
|
-
Username: state?.metadata
|
|
457
|
-
});
|
|
458
|
-
await client.send(command);
|
|
459
|
-
await this.#cookie.write(cookies, {
|
|
460
|
-
state: 'unauthenticated',
|
|
461
|
-
user: undefined,
|
|
462
|
-
});
|
|
463
|
-
return {
|
|
464
|
-
...await this.getMachineState(cookies),
|
|
465
|
-
message: `Password set. Please try signing in with your new password.`
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
catch (error) {
|
|
469
|
-
return {
|
|
470
|
-
errors: [{
|
|
471
|
-
message: error.message
|
|
472
|
-
}]
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
else {
|
|
477
|
-
return { errors: [{
|
|
478
|
-
message: 'Unrecognized authentication action.'
|
|
479
|
-
}]
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
buildApi() {
|
|
484
|
-
return withContext(context => ({
|
|
485
|
-
getState: () => this.getMachineState(context.cookies),
|
|
486
|
-
setState: (options) => this.setMachineState(context.cookies, options),
|
|
487
|
-
getCurrentUser: async () => {
|
|
488
|
-
const state = await this.#cookie.read(context.cookies);
|
|
489
|
-
if (state?.state === 'authenticated') {
|
|
490
|
-
return state.user;
|
|
491
|
-
}
|
|
492
|
-
else {
|
|
493
|
-
return null;
|
|
494
|
-
}
|
|
495
|
-
},
|
|
496
|
-
requireCurrentUser: async () => {
|
|
497
|
-
const state = await this.#cookie.read(context.cookies);
|
|
498
|
-
if (state?.state === 'authenticated') {
|
|
499
|
-
return state.user;
|
|
500
|
-
}
|
|
501
|
-
else {
|
|
502
|
-
throw new Error("Unauthorized.");
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}));
|
|
506
|
-
}
|
|
507
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { Resource } from 'wirejs-resources';
|
|
2
|
-
export declare class FileService extends Resource {
|
|
3
|
-
constructor(scope: Resource | string, id: string);
|
|
4
|
-
read(filename: string, encoding?: BufferEncoding): Promise<string>;
|
|
5
|
-
write(filename: string, data: string, { onlyIfNotExists }?: {
|
|
6
|
-
onlyIfNotExists?: boolean | undefined;
|
|
7
|
-
}): Promise<void>;
|
|
8
|
-
delete(filename: string): Promise<void>;
|
|
9
|
-
list({ prefix }?: {
|
|
10
|
-
prefix?: string | undefined;
|
|
11
|
-
}): AsyncGenerator<string, void, unknown>;
|
|
12
|
-
isAlreadyExistsError(error: any): boolean;
|
|
13
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { env } from 'process';
|
|
2
|
-
import { S3Client, ListObjectsCommand, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
|
|
3
|
-
import { Resource, } from 'wirejs-resources';
|
|
4
|
-
import { addResource } from '../resource-collector.js';
|
|
5
|
-
const Bucket = env['BUCKET'];
|
|
6
|
-
const s3 = new S3Client();
|
|
7
|
-
export class FileService extends Resource {
|
|
8
|
-
constructor(scope, id) {
|
|
9
|
-
super(scope, id);
|
|
10
|
-
addResource('FileService', { absoluteId: this.absoluteId });
|
|
11
|
-
}
|
|
12
|
-
async read(filename, encoding = 'utf8') {
|
|
13
|
-
const Key = `${this.absoluteId}/${filename}`;
|
|
14
|
-
const command = new GetObjectCommand({ Bucket, Key });
|
|
15
|
-
const result = await s3.send(command);
|
|
16
|
-
return result.Body.transformToString(encoding);
|
|
17
|
-
}
|
|
18
|
-
async write(filename, data, { onlyIfNotExists = false } = {}) {
|
|
19
|
-
const Key = `${this.absoluteId}/${filename}`;
|
|
20
|
-
const Body = data;
|
|
21
|
-
const commandDetails = {
|
|
22
|
-
Bucket, Key, Body
|
|
23
|
-
};
|
|
24
|
-
if (onlyIfNotExists) {
|
|
25
|
-
commandDetails['IfNoneMatch'] = '*';
|
|
26
|
-
}
|
|
27
|
-
const command = new PutObjectCommand(commandDetails);
|
|
28
|
-
await s3.send(command);
|
|
29
|
-
}
|
|
30
|
-
async delete(filename) {
|
|
31
|
-
const Key = `${this.absoluteId}/${filename}`;
|
|
32
|
-
const command = new DeleteObjectCommand({
|
|
33
|
-
Bucket,
|
|
34
|
-
Key
|
|
35
|
-
});
|
|
36
|
-
await s3.send(command);
|
|
37
|
-
}
|
|
38
|
-
async *list({ prefix = '' } = {}) {
|
|
39
|
-
const Prefix = `${this.absoluteId}/${prefix}`;
|
|
40
|
-
let Marker = undefined;
|
|
41
|
-
while (true) {
|
|
42
|
-
const command = new ListObjectsCommand({
|
|
43
|
-
Bucket,
|
|
44
|
-
Prefix,
|
|
45
|
-
MaxKeys: 1000,
|
|
46
|
-
Marker
|
|
47
|
-
});
|
|
48
|
-
const result = await s3.send(command);
|
|
49
|
-
Marker = result.Marker;
|
|
50
|
-
for (const o of result.Contents || []) {
|
|
51
|
-
if (o.Key) {
|
|
52
|
-
yield o.Key.slice(`${this.absoluteId}/`.length);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
if (!Marker)
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
isAlreadyExistsError(error) {
|
|
60
|
-
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html
|
|
61
|
-
return error?.$metadata?.httpStatusCode === 412;
|
|
62
|
-
}
|
|
63
|
-
}
|