Skip to content

Commit 21dc57c

Browse files
Sequelize non-admin and token refresh (#125)
1 parent da413a0 commit 21dc57c

7 files changed

Lines changed: 617 additions & 285 deletions

File tree

.github/workflows/typescript-sequelize-integ-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ jobs:
5252
env:
5353
CLUSTER_ENDPOINT: ${{ secrets.TYPESCRIPT_SEQUELIZE_CLUSTER_ENDPOINT}}
5454
REGION: ${{ secrets.TYPESCRIPT_SEQUELIZE_CLUSTER_REGION}}
55+
CLUSTER_USER: 'admin'
5556
run: |
5657
npm install
5758
npm run test

typescript/sequelize/README.md

Lines changed: 293 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,314 @@
1-
# Aurora DSQL Sequelize code examples
1+
# Sequelize with Aurora DSQL
22

33
## Overview
44

5-
The code examples in this topic show you how to use Sequelize with Aurora DSQL.
5+
This code example demonstrates how to use Sequelize with Amazon Aurora DSQL. The example shows you how to
6+
connect to an Aurora DSQL cluster with Sequelize using node-postgres, create entities, and read and write to those entity tables.
67

7-
## Run the examples
8+
Aurora DSQL is a distributed SQL database service that provides high availability and scalability for
9+
your PostgreSQL-compatible applications. Sequelize is a popular object-relational mapping framework for TypeScript that allows
10+
you to persist TypeScript objects to a database while abstracting the database interactions.
11+
12+
## About the code example
13+
14+
The example demonstrates a flexible connection approach that works for both admin and non-admin users:
15+
16+
* When connecting as an **admin user**, the example uses the `public` schema and generates an admin authentication
17+
token.
18+
* When connecting as a **non-admin user**, the example uses a custom `myschema` schema and generates a standard
19+
authentication token.
20+
21+
The code automatically detects the user type and adjusts its behavior accordingly.
22+
23+
## ⚠️ Important
24+
25+
* Running this code might result in charges to your AWS account.
26+
* We recommend that you grant your code least privilege. At most, grant only the
27+
minimum permissions required to perform the task. For more information, see
28+
[Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege).
29+
* This code is not tested in every AWS Region. For more information, see
30+
[AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services).
31+
32+
## Run the example
833

934
### Prerequisites
1035

11-
* NodeJS version >=18.0 is needed
36+
* You must have an AWS account, and have your default credentials and AWS Region
37+
configured as described in the
38+
[Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/credref/latest/refdocs/creds-config-files.html)
39+
guide.
40+
* [TypeScript](https://www.typescriptlang.org/): Ensure you have TypeScript 5.6+ installed
41+
42+
```bash
43+
npx tsc --version
44+
```
45+
It should output something similar to `Version 5.6.x` or higher.
46+
47+
* You must have an Aurora DSQL cluster. For information about creating an Aurora DSQL cluster, see the
48+
[Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html)
49+
guide.
50+
* If connecting as a non-admin user, ensure the user is linked to an IAM role and is granted access to the `myschema`
51+
schema. See the
52+
[Using database roles with IAM roles](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/using-database-and-iam-roles.html)
53+
guide.
54+
55+
### Download the Amazon root certificate from the official trust store
56+
57+
Download the Amazon root certificate from the official trust store. This example shows one of the available certs that
58+
can be used by the client. Other certs such as AmazonRootCA2.pem, AmazonRootCA3.pem, etc. can also be used.
59+
60+
```
61+
wget https://www.amazontrust.com/repository/AmazonRootCA1.pem -O root.pem
62+
```
63+
64+
### Run the code
65+
66+
The example demonstrates the following operations:
67+
68+
- Opening a connection pool to an Aurora DSQL cluster using Sequelize
69+
- Creating several Sequelize models
70+
- Creating and querying objects that are persisted in DSQL
71+
72+
The example is designed to work with both admin and non-admin users:
1273

74+
- When run as an admin user, it uses the `public` schema
75+
- When run as a non-admin user, it uses the `myschema` schema
1376

14-
### Setup test running environment
77+
**Note:** running the example will use actual resources in your AWS account and may incur charges.
1578

16-
```sh
79+
Set environment variables for your cluster details:
80+
81+
```bash
82+
# e.g. "admin"
83+
export CLUSTER_USER="<your user>"
84+
85+
# e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws"
86+
export CLUSTER_ENDPOINT="<your endpoint>"
87+
88+
# e.g. "us-east-1"
89+
export REGION="<your region>"
90+
```
91+
92+
Run the example:
93+
94+
```bash
1795
npm install
96+
npm run build
97+
npm run start
98+
```
99+
100+
The example contains comments explaining the code and the operations being performed.
101+
102+
## Sequelize Pet Clinic with DSQL
103+
104+
### Connect to an Aurora DSQL cluster
105+
106+
The example below shows how to create a Sequelize instance and connect to a DSQL cluster. It handles token generation,
107+
creating a new token for each connection to DSQL. This ensures that the token is always valid. This is done using Sequelize hooks
108+
to modify the password before each connection is created. It also has a hook after connecting to set the search path
109+
to the correct schema if we are using the non-admin user. When using Sequelize with the Postgres dialect option Sequelize
110+
uses [node-postgres](https://node-postgres.com/) to connect.
111+
112+
> **Note**
113+
>
114+
> In the dialect options you must set `clientMinMessages` to ignore, or an error will occur.
115+
116+
```ts
117+
import { DsqlSigner } from "@aws-sdk/dsql-signer";
118+
import { Sequelize, DataTypes, Model } from 'sequelize';
119+
120+
const ADMIN = "admin";
121+
const NON_ADMIN_SCHEMA = "myschema";
122+
123+
async function getSequelizeConnection(): Promise<Sequelize> {
124+
125+
const clusterEndpoint: string = process.env.CLUSTER_ENDPOINT!;
126+
const user: string = process.env.CLUSTER_USER!;
127+
const region: string = process.env.REGION!;
128+
129+
return new Sequelize("postgres", user, "", {
130+
host: clusterEndpoint,
131+
port: 5432,
132+
dialect: 'postgres', // Indicates to use node-postgres and Postgres Sequelize configuration
133+
logging: console.log, // Set to console.log to see SQL queries
134+
define: {
135+
timestamps: false
136+
},
137+
dialectOptions: {
138+
user: user,
139+
clientMinMessages: 'ignore', // This is essential
140+
skipIndexes: true,
141+
ssl: {
142+
mode: 'verify-full'
143+
},
144+
},
145+
pool: { // Connection pool configuration options
146+
max: 5,
147+
min: 0,
148+
acquire: 30000,
149+
idle: 10000
150+
},
151+
hooks: {
152+
beforeConnect: async (config) => {
153+
// This runs before a connection is established, creating a fresh token.
154+
const token = await getPasswordToken(clusterEndpoint, user, region);
155+
config.password = token;
156+
},
157+
afterConnect: async (connection, config) => {
158+
if (user !== ADMIN) {
159+
await (connection as any).query(`SET search_path TO ${NON_ADMIN_SCHEMA}`);
160+
}
161+
}
162+
}
163+
})
164+
}
165+
166+
async function getPasswordToken(endpoint: string, user: string, region: string): Promise<string> {
167+
const signer = new DsqlSigner({
168+
hostname: endpoint,
169+
region,
170+
});
171+
if (user === ADMIN) {
172+
return await signer.getDbConnectAdminAuthToken();
173+
} else {
174+
(signer as any).user = user;
175+
let token = await signer.getDbConnectAuthToken();
176+
return token;
177+
}
178+
}
18179
```
19180

20-
### Run the example tests
181+
#### Connection Pooling
182+
In Sequelize, [connection pooling](https://sequelize.org/docs/v6/other-topics/connection-pool/) can be used by specifying
183+
a connection pool configuration in the constructor, as seen in the `pool` parameter. In the example above, a new token is created
184+
for each connection opened in the connection pool. Note that DSQL connections will automatically close after one hour. The
185+
connection pool will open new connections as needed.
186+
187+
### Create models
188+
189+
#### Using UUID as Primary Key
190+
191+
DSQL does not support serialized primary keys or identity columns (auto-incrementing integers) that are commonly used in traditional relational databases. Instead, it is recommended to use UUID (Universally Unique Identifier) as the primary key for your entities.
192+
193+
Here's how to define a UUID primary key in your entity class:
194+
```ts
195+
id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
196+
```
197+
198+
#### Sequelize.sync() does not work with DSQL
199+
200+
Attempting to create or modify tables using `Sequelize.sync()` will result in an error. This can be worked around by creating tables in advance separately using the `QueryInterface`. The `QueryInterface.createTable()` function allows table creation, and `QueryInterface.query()` allows arbitrary SQL statements to be executed, including schema modification or index creation. Note that if you create tables directly using the Query Interface, you still need to initialize the model, as shown in the example below. This initializes the model in memory for Sequelize execution, whereas the Query Interface interacts with the database.
201+
202+
#### Model definitions
21203

22-
```sh
23-
export CLUSTER_ENDPOINT="<your cluster endpoint>"
24-
export REGION="<your cluster region>"
204+
```ts
205+
class Owner extends Model {
206+
declare id: string;
207+
declare name: string;
208+
declare city: string;
209+
declare telephone: string | null;
210+
}
25211

26-
npm run test
212+
class Pet extends Model {
213+
declare id: string;
214+
declare name: string;
215+
declare birthDate: Date;
216+
declare ownerId: string | null;
217+
}
218+
219+
class VetSpecialties extends Model {
220+
declare id: string;
221+
declare vetId: string | null;
222+
declare specialtyId: string | null;
223+
}
224+
225+
class Specialty extends Model {
226+
declare id: string;
227+
}
228+
229+
class Vet extends Model {
230+
declare id: string;
231+
declare name: string;
232+
declare Specialties?: Specialty[];
233+
declare setSpecialties: (specialties: Specialty[]) => Promise<void>;
234+
}
235+
236+
async function createTables(sequelize: Sequelize) {
237+
// Create tables in DB - workaround for Sequelize.sync()
238+
await queryInterface.createTable('owner', {
239+
id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
240+
name: { type: DataTypes.STRING(30), allowNull: false },
241+
city: { type: DataTypes.STRING(80), allowNull: false },
242+
telephone: { type: DataTypes.STRING(20), allowNull: true, defaultValue: null }
243+
});
244+
245+
await queryInterface.createTable('pet', {
246+
id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
247+
name: { type: DataTypes.STRING(30), allowNull: false },
248+
birthDate: { type: DataTypes.DATEONLY, allowNull: false },
249+
ownerId: { type: DataTypes.UUID, allowNull: true }
250+
});
251+
252+
await queryInterface.createTable('specialty', {
253+
id: { type: DataTypes.STRING(80), primaryKey: true, field: 'name' }
254+
});
255+
256+
await queryInterface.createTable('vet', {
257+
id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
258+
name: { type: DataTypes.STRING(30), allowNull: false }
259+
});
260+
261+
await queryInterface.createTable('vetSpecialties', {
262+
id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
263+
vetId: { type: DataTypes.UUID, allowNull: true },
264+
specialtyId: { type: DataTypes.STRING(80), allowNull: true }
265+
});
266+
267+
// Initialize Sequelize models in memory
268+
Owner.init({
269+
id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
270+
name: { type: DataTypes.STRING(30), allowNull: false },
271+
city: { type: DataTypes.STRING(80), allowNull: false },
272+
telephone: { type: DataTypes.STRING(20), allowNull: true, defaultValue: null }
273+
}, { sequelize, tableName: 'owner' });
274+
275+
Pet.init({
276+
id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
277+
name: { type: DataTypes.STRING(30), allowNull: false },
278+
birthDate: { type: DataTypes.DATEONLY, allowNull: false },
279+
ownerId: { type: DataTypes.UUID, allowNull: true }
280+
}, { sequelize, tableName: 'pet', });
281+
282+
VetSpecialties.init({
283+
id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
284+
vetId: { type: DataTypes.UUID, allowNull: true },
285+
specialtyId: { type: DataTypes.STRING(80), allowNull: true }
286+
}, { sequelize, tableName: 'vetSpecialties', });
287+
288+
Specialty.init({
289+
id: { type: DataTypes.STRING(80), primaryKey: true, field: 'name' }
290+
}, { sequelize, tableName: 'specialty', });
291+
292+
Vet.init({
293+
id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
294+
name: { type: DataTypes.STRING(30), allowNull: false }
295+
}, { sequelize, tableName: 'vet', });
296+
297+
// Create relationships, note that constraints must be set to false.
298+
Pet.belongsTo(Owner, { foreignKey: 'ownerId', constraints: false });
299+
Owner.hasMany(Pet, { foreignKey: 'ownerId', constraints: false });
300+
Vet.belongsToMany(Specialty, { through: VetSpecialties, foreignKey: 'vetId', otherKey: 'specialtyId', constraints: false });
301+
Specialty.belongsToMany(Vet, { through: VetSpecialties, foreignKey: 'specialtyId', otherKey: 'vetId', constraints: false, as: 'Specialties' });
302+
}
27303
```
28304

305+
## Additional resources
306+
307+
* [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/what-is-aurora-dsql.html)
308+
* [Sequelize Documentation](https://sequelize.org/docs/v6/)
309+
* [AWS SDK for JavaScript Documentation](https://docs.aws.amazon.com/sdk-for-javascript/)
29310
---
30311

31312
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
32313

33-
SPDX-License-Identifier: MIT-0
314+
SPDX-License-Identifier: MIT-0

typescript/sequelize/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@
2323
"jest": "^29.7.0",
2424
"typescript": "^5.0.0"
2525
}
26-
}
26+
}

0 commit comments

Comments
 (0)