wirejs-deploy-amplify-basic 0.0.69-table-resource → 0.0.71-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.
@@ -7,8 +7,7 @@ import { Bucket, BlockPublicAccess } from 'aws-cdk-lib/aws-s3';
7
7
  import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb';
8
8
  import { api } from './functions/api/resource';
9
9
  import { auth } from './auth/resource';
10
- import { KeyFieldDefinition } from 'wirejs-resources';
11
- import type { TableDefinition } from '../wirejs-resources-overrides/resources/distributed-table';
10
+ import { TableDefinition } from 'wirejs-resources';
12
11
 
13
12
  // @ts-ignore
14
13
  import generated from './generated-resources';
@@ -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.37-table-resource"
6
+ "wirejs-resources": "^0.1.39-table-resource"
7
7
  }
8
8
  }
@@ -1,11 +1,5 @@
1
1
  import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
2
- import { AllIndexesByName, KeyCondition, IndexFieldNames, Filter, Index, KeyFieldDefinition, KindaPretty, Parser, RecordKey, Resource, DistributedTable as BaseDistributedTable } from 'wirejs-resources';
3
- export type TableDefinition = {
4
- absoluteId: string;
5
- partitionKey: KeyFieldDefinition;
6
- sortKey?: KeyFieldDefinition;
7
- indexes?: Index<any>[];
8
- };
2
+ import { AllIndexesByName, KeyCondition, IndexFieldNames, Filter, Index, KindaPretty, Parser, RecordKey, Resource, DistributedTable as BaseDistributedTable } from 'wirejs-resources';
9
3
  /**
10
4
  * A table of records that favors very high *overall* scalability at the expense of
11
5
  * scalability *between* partitions. Providers will distribute your data across many
@@ -1,7 +1,7 @@
1
1
  import { env } from 'process';
2
2
  import { DynamoDBClient, } from '@aws-sdk/client-dynamodb';
3
3
  import { PutCommand, GetCommand, QueryCommand, ScanCommand, DeleteCommand, } from '@aws-sdk/lib-dynamodb';
4
- import { Resource, indexName, } from 'wirejs-resources';
4
+ import { Resource, indexName } from 'wirejs-resources';
5
5
  import { addResource } from '../resource-collector.js';
6
6
  function isFieldComparison(filter) {
7
7
  return !['and', 'or', 'not'].some(key => key in filter);
@@ -99,6 +99,7 @@ export class DistributedTable extends Resource {
99
99
  super(scope, id);
100
100
  this.parse = options.parse;
101
101
  this.key = options.key;
102
+ this.indexes = options.indexes;
102
103
  this.ddbClient = new DynamoDBClient();
103
104
  this.table = env['TABLE_NAME_PREFIX'] + this.absoluteId.replace(/[^a-zA-Z0-9-_]/g, '_');
104
105
  const resourceDefinition = {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
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
+ });
@@ -0,0 +1 @@
1
+ export * from 'wirejs-resources/client';
@@ -0,0 +1 @@
1
+ export * from 'wirejs-resources/client';
@@ -0,0 +1,4 @@
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';
@@ -0,0 +1,14 @@
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;
@@ -0,0 +1 @@
1
+ export * from 'wirejs-resources/internal';
@@ -0,0 +1 @@
1
+ export * from 'wirejs-resources/internal';
@@ -0,0 +1 @@
1
+ export declare function addResource(type: string, options: any): void;
@@ -0,0 +1,7 @@
1
+ globalThis.wirejsResources = [];
2
+ export function addResource(type, options) {
3
+ globalThis.wirejsResources.push({
4
+ type,
5
+ options
6
+ });
7
+ }
@@ -0,0 +1,41 @@
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
+ }
@@ -0,0 +1,267 @@
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
+ }
@@ -0,0 +1,19 @@
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
+ }
@@ -0,0 +1,507 @@
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
+ }
@@ -0,0 +1,13 @@
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
+ }
@@ -0,0 +1,63 @@
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wirejs-deploy-amplify-basic",
3
- "version": "0.0.69-table-resource",
3
+ "version": "0.0.71-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.37-table-resource"
35
+ "wirejs-resources": "^0.1.39-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
+ "build": "tsc",
43
+ "test": "node --import tsx --test test/**/*.test.ts"
43
44
  },
44
45
  "files": [
45
46
  "amplify-backend-assets/*",