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.
@@ -3,6 +3,6 @@
3
3
  "dependencies": {
4
4
  "jsdom": "^25.0.1",
5
5
  "wirejs-dom": "^1.0.38",
6
- "wirejs-resources": "^0.1.39-table-resource"
6
+ "wirejs-resources": "^0.1.40-table-resource"
7
7
  }
8
8
  }
@@ -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 [field] = Object.keys(filter);
24
- const condition = filter[field];
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} = :${field}`;
31
+ return `${field} = :${baseField}`;
27
32
  if ('ne' in condition)
28
- return `${field} <> :${field}`;
33
+ return `${field} <> :${baseField}`;
29
34
  if ('gt' in condition)
30
- return `${field} > :${field}`;
35
+ return `${field} > :${baseField}`;
31
36
  if ('ge' in condition)
32
- return `${field} >= :${field}`;
37
+ return `${field} >= :${baseField}`;
33
38
  if ('lt' in condition)
34
- return `${field} < :${field}`;
39
+ return `${field} < :${baseField}`;
35
40
  if ('le' in condition)
36
- return `${field} <= :${field}`;
41
+ return `${field} <= :${baseField}`;
37
42
  if ('between' in condition)
38
- return `${field} BETWEEN :${field}Low AND :${field}High`;
43
+ return `${field} BETWEEN :${baseField}Low AND :${baseField}High`;
39
44
  if ('beginsWith' in condition)
40
- return `begins_with(${field}, :${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]) => buildFilterExpression({ [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.71-table-resource",
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.39-table-resource"
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
- "build": "tsc",
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,4 +0,0 @@
1
- export * from 'wirejs-resources';
2
- export { FileService } from './services/file.js';
3
- export { AuthenticationService } from './services/authentication.js';
4
- export { DistributedTable } from './resources/distributed-table.js';
@@ -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,7 +0,0 @@
1
- globalThis.wirejsResources = [];
2
- export function addResource(type, options) {
3
- globalThis.wirejsResources.push({
4
- type,
5
- options
6
- });
7
- }
@@ -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
- }