@lti-tool/postgresql 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @lti-tool/postgresql
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - f95c631: Initial production release of PostgreSQL storage adapter
8
+
9
+ ### Patch Changes
10
+
11
+ - ed13d10: Package version updates
12
+ - Updated dependencies [ed13d10]
13
+ - @lti-tool/core@1.0.4
@@ -0,0 +1,18 @@
1
+ services:
2
+ postgres:
3
+ image: postgres:16
4
+ environment:
5
+ POSTGRES_USER: lti_user
6
+ POSTGRES_PASSWORD: lti_password
7
+ POSTGRES_DB: lti_test
8
+ ports:
9
+ - '5432:5432'
10
+ volumes:
11
+ - postgres_data:/var/lib/postgresql/data
12
+ healthcheck:
13
+ test: ['CMD-SHELL', 'pg_isready -U lti_user -d lti_test']
14
+ timeout: 5s
15
+ retries: 10
16
+
17
+ volumes:
18
+ postgres_data:
@@ -0,0 +1,42 @@
1
+ CREATE TABLE "clients" (
2
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3
+ "name" varchar(255) NOT NULL,
4
+ "iss" varchar(255) NOT NULL,
5
+ "client_id" varchar(255) NOT NULL,
6
+ "auth_url" text NOT NULL,
7
+ "token_url" text NOT NULL,
8
+ "jwks_url" text NOT NULL
9
+ );
10
+ --> statement-breakpoint
11
+ CREATE TABLE "deployments" (
12
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
13
+ "deployment_id" varchar(255) NOT NULL,
14
+ "name" varchar(255),
15
+ "description" text,
16
+ "client_id" uuid NOT NULL
17
+ );
18
+ --> statement-breakpoint
19
+ CREATE TABLE "nonces" (
20
+ "nonce" varchar(255) PRIMARY KEY NOT NULL,
21
+ "expires_at" timestamp with time zone NOT NULL
22
+ );
23
+ --> statement-breakpoint
24
+ CREATE TABLE "registration_sessions" (
25
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
26
+ "data" jsonb NOT NULL,
27
+ "expires_at" timestamp with time zone NOT NULL
28
+ );
29
+ --> statement-breakpoint
30
+ CREATE TABLE "sessions" (
31
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
32
+ "data" jsonb NOT NULL,
33
+ "expires_at" timestamp with time zone NOT NULL
34
+ );
35
+ --> statement-breakpoint
36
+ ALTER TABLE "deployments" ADD CONSTRAINT "deployments_client_id_clients_id_fk" FOREIGN KEY ("client_id") REFERENCES "public"."clients"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
37
+ CREATE INDEX "issuer_client_idx" ON "clients" USING btree ("client_id","iss");--> statement-breakpoint
38
+ CREATE UNIQUE INDEX "iss_client_id_unique" ON "clients" USING btree ("iss","client_id");--> statement-breakpoint
39
+ CREATE INDEX "deployment_id_idx" ON "deployments" USING btree ("deployment_id");--> statement-breakpoint
40
+ CREATE UNIQUE INDEX "client_deployment_unique" ON "deployments" USING btree ("client_id","deployment_id");--> statement-breakpoint
41
+ CREATE INDEX "reg_sessions_expires_at_idx" ON "registration_sessions" USING btree ("expires_at");--> statement-breakpoint
42
+ CREATE INDEX "sessions_expires_at_idx" ON "sessions" USING btree ("expires_at");
@@ -0,0 +1,330 @@
1
+ {
2
+ "id": "7e071465-0b94-4f8d-a120-45ddb5575592",
3
+ "prevId": "00000000-0000-0000-0000-000000000000",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "public.clients": {
8
+ "name": "clients",
9
+ "schema": "",
10
+ "columns": {
11
+ "id": {
12
+ "name": "id",
13
+ "type": "uuid",
14
+ "primaryKey": true,
15
+ "notNull": true,
16
+ "default": "gen_random_uuid()"
17
+ },
18
+ "name": {
19
+ "name": "name",
20
+ "type": "varchar(255)",
21
+ "primaryKey": false,
22
+ "notNull": true
23
+ },
24
+ "iss": {
25
+ "name": "iss",
26
+ "type": "varchar(255)",
27
+ "primaryKey": false,
28
+ "notNull": true
29
+ },
30
+ "client_id": {
31
+ "name": "client_id",
32
+ "type": "varchar(255)",
33
+ "primaryKey": false,
34
+ "notNull": true
35
+ },
36
+ "auth_url": {
37
+ "name": "auth_url",
38
+ "type": "text",
39
+ "primaryKey": false,
40
+ "notNull": true
41
+ },
42
+ "token_url": {
43
+ "name": "token_url",
44
+ "type": "text",
45
+ "primaryKey": false,
46
+ "notNull": true
47
+ },
48
+ "jwks_url": {
49
+ "name": "jwks_url",
50
+ "type": "text",
51
+ "primaryKey": false,
52
+ "notNull": true
53
+ }
54
+ },
55
+ "indexes": {
56
+ "issuer_client_idx": {
57
+ "name": "issuer_client_idx",
58
+ "columns": [
59
+ {
60
+ "expression": "client_id",
61
+ "isExpression": false,
62
+ "asc": true,
63
+ "nulls": "last"
64
+ },
65
+ {
66
+ "expression": "iss",
67
+ "isExpression": false,
68
+ "asc": true,
69
+ "nulls": "last"
70
+ }
71
+ ],
72
+ "isUnique": false,
73
+ "concurrently": false,
74
+ "method": "btree",
75
+ "with": {}
76
+ },
77
+ "iss_client_id_unique": {
78
+ "name": "iss_client_id_unique",
79
+ "columns": [
80
+ {
81
+ "expression": "iss",
82
+ "isExpression": false,
83
+ "asc": true,
84
+ "nulls": "last"
85
+ },
86
+ {
87
+ "expression": "client_id",
88
+ "isExpression": false,
89
+ "asc": true,
90
+ "nulls": "last"
91
+ }
92
+ ],
93
+ "isUnique": true,
94
+ "concurrently": false,
95
+ "method": "btree",
96
+ "with": {}
97
+ }
98
+ },
99
+ "foreignKeys": {},
100
+ "compositePrimaryKeys": {},
101
+ "uniqueConstraints": {},
102
+ "policies": {},
103
+ "checkConstraints": {},
104
+ "isRLSEnabled": false
105
+ },
106
+ "public.deployments": {
107
+ "name": "deployments",
108
+ "schema": "",
109
+ "columns": {
110
+ "id": {
111
+ "name": "id",
112
+ "type": "uuid",
113
+ "primaryKey": true,
114
+ "notNull": true,
115
+ "default": "gen_random_uuid()"
116
+ },
117
+ "deployment_id": {
118
+ "name": "deployment_id",
119
+ "type": "varchar(255)",
120
+ "primaryKey": false,
121
+ "notNull": true
122
+ },
123
+ "name": {
124
+ "name": "name",
125
+ "type": "varchar(255)",
126
+ "primaryKey": false,
127
+ "notNull": false
128
+ },
129
+ "description": {
130
+ "name": "description",
131
+ "type": "text",
132
+ "primaryKey": false,
133
+ "notNull": false
134
+ },
135
+ "client_id": {
136
+ "name": "client_id",
137
+ "type": "uuid",
138
+ "primaryKey": false,
139
+ "notNull": true
140
+ }
141
+ },
142
+ "indexes": {
143
+ "deployment_id_idx": {
144
+ "name": "deployment_id_idx",
145
+ "columns": [
146
+ {
147
+ "expression": "deployment_id",
148
+ "isExpression": false,
149
+ "asc": true,
150
+ "nulls": "last"
151
+ }
152
+ ],
153
+ "isUnique": false,
154
+ "concurrently": false,
155
+ "method": "btree",
156
+ "with": {}
157
+ },
158
+ "client_deployment_unique": {
159
+ "name": "client_deployment_unique",
160
+ "columns": [
161
+ {
162
+ "expression": "client_id",
163
+ "isExpression": false,
164
+ "asc": true,
165
+ "nulls": "last"
166
+ },
167
+ {
168
+ "expression": "deployment_id",
169
+ "isExpression": false,
170
+ "asc": true,
171
+ "nulls": "last"
172
+ }
173
+ ],
174
+ "isUnique": true,
175
+ "concurrently": false,
176
+ "method": "btree",
177
+ "with": {}
178
+ }
179
+ },
180
+ "foreignKeys": {
181
+ "deployments_client_id_clients_id_fk": {
182
+ "name": "deployments_client_id_clients_id_fk",
183
+ "tableFrom": "deployments",
184
+ "tableTo": "clients",
185
+ "columnsFrom": ["client_id"],
186
+ "columnsTo": ["id"],
187
+ "onDelete": "no action",
188
+ "onUpdate": "no action"
189
+ }
190
+ },
191
+ "compositePrimaryKeys": {},
192
+ "uniqueConstraints": {},
193
+ "policies": {},
194
+ "checkConstraints": {},
195
+ "isRLSEnabled": false
196
+ },
197
+ "public.nonces": {
198
+ "name": "nonces",
199
+ "schema": "",
200
+ "columns": {
201
+ "nonce": {
202
+ "name": "nonce",
203
+ "type": "varchar(255)",
204
+ "primaryKey": true,
205
+ "notNull": true
206
+ },
207
+ "expires_at": {
208
+ "name": "expires_at",
209
+ "type": "timestamp with time zone",
210
+ "primaryKey": false,
211
+ "notNull": true
212
+ }
213
+ },
214
+ "indexes": {},
215
+ "foreignKeys": {},
216
+ "compositePrimaryKeys": {},
217
+ "uniqueConstraints": {},
218
+ "policies": {},
219
+ "checkConstraints": {},
220
+ "isRLSEnabled": false
221
+ },
222
+ "public.registration_sessions": {
223
+ "name": "registration_sessions",
224
+ "schema": "",
225
+ "columns": {
226
+ "id": {
227
+ "name": "id",
228
+ "type": "uuid",
229
+ "primaryKey": true,
230
+ "notNull": true,
231
+ "default": "gen_random_uuid()"
232
+ },
233
+ "data": {
234
+ "name": "data",
235
+ "type": "jsonb",
236
+ "primaryKey": false,
237
+ "notNull": true
238
+ },
239
+ "expires_at": {
240
+ "name": "expires_at",
241
+ "type": "timestamp with time zone",
242
+ "primaryKey": false,
243
+ "notNull": true
244
+ }
245
+ },
246
+ "indexes": {
247
+ "reg_sessions_expires_at_idx": {
248
+ "name": "reg_sessions_expires_at_idx",
249
+ "columns": [
250
+ {
251
+ "expression": "expires_at",
252
+ "isExpression": false,
253
+ "asc": true,
254
+ "nulls": "last"
255
+ }
256
+ ],
257
+ "isUnique": false,
258
+ "concurrently": false,
259
+ "method": "btree",
260
+ "with": {}
261
+ }
262
+ },
263
+ "foreignKeys": {},
264
+ "compositePrimaryKeys": {},
265
+ "uniqueConstraints": {},
266
+ "policies": {},
267
+ "checkConstraints": {},
268
+ "isRLSEnabled": false
269
+ },
270
+ "public.sessions": {
271
+ "name": "sessions",
272
+ "schema": "",
273
+ "columns": {
274
+ "id": {
275
+ "name": "id",
276
+ "type": "uuid",
277
+ "primaryKey": true,
278
+ "notNull": true,
279
+ "default": "gen_random_uuid()"
280
+ },
281
+ "data": {
282
+ "name": "data",
283
+ "type": "jsonb",
284
+ "primaryKey": false,
285
+ "notNull": true
286
+ },
287
+ "expires_at": {
288
+ "name": "expires_at",
289
+ "type": "timestamp with time zone",
290
+ "primaryKey": false,
291
+ "notNull": true
292
+ }
293
+ },
294
+ "indexes": {
295
+ "sessions_expires_at_idx": {
296
+ "name": "sessions_expires_at_idx",
297
+ "columns": [
298
+ {
299
+ "expression": "expires_at",
300
+ "isExpression": false,
301
+ "asc": true,
302
+ "nulls": "last"
303
+ }
304
+ ],
305
+ "isUnique": false,
306
+ "concurrently": false,
307
+ "method": "btree",
308
+ "with": {}
309
+ }
310
+ },
311
+ "foreignKeys": {},
312
+ "compositePrimaryKeys": {},
313
+ "uniqueConstraints": {},
314
+ "policies": {},
315
+ "checkConstraints": {},
316
+ "isRLSEnabled": false
317
+ }
318
+ },
319
+ "enums": {},
320
+ "schemas": {},
321
+ "sequences": {},
322
+ "roles": {},
323
+ "policies": {},
324
+ "views": {},
325
+ "_meta": {
326
+ "columns": {},
327
+ "schemas": {},
328
+ "tables": {}
329
+ }
330
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1770007114581,
9
+ "tag": "0000_black_mentallo",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,11 @@
1
+ import 'dotenv/config';
2
+ import { defineConfig } from 'drizzle-kit';
3
+
4
+ export default defineConfig({
5
+ out: './drizzle',
6
+ schema: './src/db/schema',
7
+ dialect: 'postgresql',
8
+ dbCredentials: {
9
+ url: process.env.DATABASE_URL!,
10
+ },
11
+ });
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@lti-tool/postgresql",
3
+ "version": "1.0.0",
4
+ "description": "PostgreSQL storage for LTI 1.3 @lti-tool",
5
+ "keywords": [
6
+ "lti",
7
+ "lti13",
8
+ "education",
9
+ "storage",
10
+ "postgresql",
11
+ "postgres",
12
+ "edtech",
13
+ "serverless"
14
+ ],
15
+ "author": "jamesjoplin",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/lti-tool/lti-tool.git",
20
+ "directory": "packages/postgresql"
21
+ },
22
+ "homepage": "https://github.com/lti-tool/lti-tool#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/lti-tool/lti-tool/issues"
25
+ },
26
+ "type": "module",
27
+ "main": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.js"
33
+ }
34
+ },
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "dev": "tsc --watch",
38
+ "test": "vitest"
39
+ },
40
+ "dependencies": {
41
+ "@lti-tool/core": "*",
42
+ "drizzle-orm": "^0.45.1",
43
+ "lru-cache": "^11.2.5",
44
+ "postgres": "^3.4.8"
45
+ },
46
+ "peerDependencies": {
47
+ "pino": "^9.9.0"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "pino": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "devDependencies": {
55
+ "dotenv": "^17.2.3",
56
+ "drizzle-kit": "^0.31.8",
57
+ "pino": "^10.3.0"
58
+ }
59
+ }
@@ -0,0 +1,23 @@
1
+ import type { LTILaunchConfig, LTISession } from '@lti-tool/core';
2
+ import { LRUCache } from 'lru-cache';
3
+
4
+ export const LAUNCH_CONFIG_CACHE = new LRUCache<
5
+ string,
6
+ LTILaunchConfig | undefinedLaunchConfig
7
+ >({
8
+ max: 1000,
9
+ ttl: 1000 * 60 * 15, // 15 minutes
10
+ });
11
+ export const SESSION_CACHE = new LRUCache<string, LTISession | undefinedSession>({
12
+ max: 1000,
13
+ ttl: 1000 * 60 * 5, // 5 minutes (shorter than clients)
14
+ });
15
+
16
+ export const SESSION_TTL = 60 * 60 * 24; // session ttl is one day
17
+ export const NONCE_TTL = 60 * 15; // nonce ttl is fifteen minutes
18
+
19
+ // we need an undefined value to handle cache misses and cache them
20
+ export const undefinedLaunchConfigValue = Symbol('undefinedLaunchConfig');
21
+ export type undefinedLaunchConfig = typeof undefinedLaunchConfigValue;
22
+ export const undefinedSessionValue = Symbol('undefinedSession');
23
+ export type undefinedSession = typeof undefinedSessionValue;
@@ -0,0 +1,18 @@
1
+ import { index, pgTable, text, uniqueIndex, uuid, varchar } from 'drizzle-orm/pg-core';
2
+
3
+ export const clientsTable = pgTable(
4
+ 'clients',
5
+ {
6
+ id: uuid('id').primaryKey().defaultRandom(),
7
+ name: varchar('name', { length: 255 }).notNull(),
8
+ iss: varchar('iss', { length: 255 }).notNull(),
9
+ clientId: varchar('client_id', { length: 255 }).notNull(),
10
+ authUrl: text('auth_url').notNull(),
11
+ tokenUrl: text('token_url').notNull(),
12
+ jwksUrl: text('jwks_url').notNull(),
13
+ },
14
+ (table) => [
15
+ index('issuer_client_idx').on(table.clientId, table.iss),
16
+ uniqueIndex('iss_client_id_unique').on(table.iss, table.clientId),
17
+ ],
18
+ );
@@ -0,0 +1,20 @@
1
+ import { index, pgTable, text, uniqueIndex, uuid, varchar } from 'drizzle-orm/pg-core';
2
+
3
+ import { clientsTable } from './clients.schema';
4
+
5
+ export const deploymentsTable = pgTable(
6
+ 'deployments',
7
+ {
8
+ id: uuid('id').primaryKey().defaultRandom(),
9
+ deploymentId: varchar('deployment_id', { length: 255 }).notNull(),
10
+ name: varchar('name', { length: 255 }),
11
+ description: text('description'),
12
+ clientId: uuid('client_id')
13
+ .notNull()
14
+ .references(() => clientsTable.id),
15
+ },
16
+ (table) => [
17
+ index('deployment_id_idx').on(table.deploymentId),
18
+ uniqueIndex('client_deployment_unique').on(table.clientId, table.deploymentId),
19
+ ],
20
+ );
@@ -0,0 +1,5 @@
1
+ export * from './clients.schema';
2
+ export * from './deployments.schema';
3
+ export * from './nonces.schema';
4
+ export * from './registrationSessions.schema';
5
+ export * from './sessions.schema';
@@ -0,0 +1,6 @@
1
+ import { pgTable, timestamp, varchar } from 'drizzle-orm/pg-core';
2
+
3
+ export const noncesTable = pgTable('nonces', {
4
+ nonce: varchar('nonce', { length: 255 }).primaryKey(),
5
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
6
+ });
@@ -0,0 +1,14 @@
1
+ import type { LTIDynamicRegistrationSession } from '@lti-tool/core';
2
+ import { index, jsonb, pgTable, timestamp, uuid } from 'drizzle-orm/pg-core';
3
+
4
+ export const registrationSessionsTable = pgTable(
5
+ 'registration_sessions',
6
+ {
7
+ id: uuid('id').primaryKey().defaultRandom(),
8
+ data: jsonb('data')
9
+ .$type<Omit<LTIDynamicRegistrationSession, 'sessionId'>>()
10
+ .notNull(),
11
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
12
+ },
13
+ (table) => [index('reg_sessions_expires_at_idx').on(table.expiresAt)],
14
+ );
@@ -0,0 +1,12 @@
1
+ import type { LTISession } from '@lti-tool/core';
2
+ import { index, jsonb, pgTable, timestamp, uuid } from 'drizzle-orm/pg-core';
3
+
4
+ export const sessionsTable = pgTable(
5
+ 'sessions',
6
+ {
7
+ id: uuid('id').primaryKey().defaultRandom(),
8
+ data: jsonb('data').$type<Omit<LTISession, 'id'>>().notNull(),
9
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
10
+ },
11
+ (table) => [index('sessions_expires_at_idx').on(table.expiresAt)],
12
+ );
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { PostgresStorage } from './postgresStorage.js';
2
+ export type { PostgresStorageConfig } from './interfaces/postgresStorageConfig.js';
@@ -0,0 +1,35 @@
1
+ import type { Logger } from 'pino';
2
+
3
+ export interface PostgresStorageConfig {
4
+ logger?: Logger;
5
+ /**
6
+ * PostgreSQL connection URL in format: postgresql://user:password@host:port/database
7
+ * Compatible with DATABASE_URL environment variable used by most ORMs
8
+ */
9
+ connectionUrl: string;
10
+ /**
11
+ * Optional postgres.js connection options
12
+ */
13
+ poolOptions?: {
14
+ /**
15
+ * Maximum number of connections in the pool.
16
+ * Defaults to 1 in serverless environments, 10 otherwise.
17
+ *
18
+ * Recommended values:
19
+ * - Serverless (Lambda, Cloud Functions): 1
20
+ * - Low traffic servers: 5-10
21
+ * - Medium traffic servers: 10-20
22
+ * - High traffic servers: 20-50
23
+ */
24
+ max?: number;
25
+ /**
26
+ * Idle timeout in seconds before a connection is closed.
27
+ * Defaults to 20 seconds.
28
+ */
29
+ idleTimeout?: number;
30
+ };
31
+ /**
32
+ * Nonce expiration time in seconds (defaults to 600 = 10 minutes)
33
+ */
34
+ nonceExpirationSeconds?: number;
35
+ }