Skip to content

Commit fb31429

Browse files
committed
feat(schemas): init org invitation tables
1 parent b064fab commit fb31429

File tree

11 files changed

+257
-2
lines changed

11 files changed

+257
-2
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,6 @@ jobs:
193193
run: node .scripts/compare-database.js fresh old
194194
# ** End **
195195

196-
- name: Check alteration databases
196+
- name: Check alteration sequence
197197
working-directory: ./fresh
198198
run: node .scripts/check-alterations-sequence.js

.scripts/check-alterations-sequence.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ const diffFiles = execSync("git diff HEAD~1 HEAD --name-only --diff-filter=ACR",
1919
});
2020
const committedAlterations = diffFiles
2121
.split("\n")
22-
.filter((filename) => filename.startsWith(alterationFilePrefix))
22+
.filter((filename) =>
23+
filename.startsWith(alterationFilePrefix) &&
24+
!filename.slice(alterationFilePrefix.length).includes("/")
25+
)
2326
.map((filename) =>
2427
filename.replace(alterationFilePrefix, "").replace(".ts", "")
2528
);
@@ -32,4 +35,6 @@ for (const alteration of committedAlterations) {
3235
`Wrong alteration sequence for committed file: ${alteration}\nAll timestamps of committed alteration files should be greater than the biggest one in the base branch.`
3336
);
3437
}
38+
39+
console.log(`✅ ${alteration}`);
3540
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { sql } from 'slonik';
2+
3+
import type { AlterationScript } from '../lib/types/alteration.js';
4+
5+
import { applyTableRls, dropTableRls } from './utils/1704934999-tables.js';
6+
7+
const alteration: AlterationScript = {
8+
up: async (pool) => {
9+
await pool.query(sql`
10+
create table magic_links (
11+
tenant_id varchar(21) not null
12+
references tenants (id) on update cascade on delete cascade,
13+
/** The unique identifier of the link. */
14+
id varchar(21) not null,
15+
/** The token that can be used to verify the link. */
16+
token varchar(32) not null,
17+
/** The time when the link was created. */
18+
created_at timestamptz not null default (now()),
19+
/** The time when the link was consumed. */
20+
consumed_at timestamptz,
21+
primary key (id)
22+
);
23+
24+
create index magic_links__token
25+
on magic_links (tenant_id, token);
26+
`);
27+
await applyTableRls(pool, 'magic_links');
28+
},
29+
down: async (pool) => {
30+
await dropTableRls(pool, 'magic_links');
31+
await pool.query(sql`
32+
drop table magic_links;
33+
`);
34+
},
35+
};
36+
37+
export default alteration;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { sql } from 'slonik';
2+
3+
import type { AlterationScript } from '../lib/types/alteration.js';
4+
5+
import { applyTableRls, dropTableRls } from './utils/1704934999-tables.js';
6+
7+
const alteration: AlterationScript = {
8+
up: async (pool) => {
9+
await pool.query(sql`
10+
create table organization_invitations (
11+
tenant_id varchar(21) not null
12+
references tenants (id) on update cascade on delete cascade,
13+
/** The unique identifier of the invitation. */
14+
id varchar(21) not null,
15+
/** The user ID who sent the invitation. */
16+
inviter_id varchar(21) not null,
17+
/** The email address or other identifier of the invitee. */
18+
invitee varchar(256) not null,
19+
/** The user ID of who accepted the invitation. */
20+
accepted_user_id varchar(21)
21+
references users (id) on update cascade on delete cascade,
22+
/** The ID of the organization to which the invitee is invited. */
23+
organization_id varchar(21) not null,
24+
/** The status of the invitation. */
25+
status varchar(32) /* @use OrganizationInvitationStatus */ not null,
26+
/** The ID of the magic link that can be used to accept the invitation. */
27+
magic_link_id varchar(21)
28+
references magic_links (id) on update cascade on delete cascade,
29+
/** The time when the invitation was created. */
30+
created_at timestamptz not null default (now()),
31+
/** The time when the invitation status was last updated. */
32+
updated_at timestamptz not null default (now()),
33+
/** The time when the invitation expires. */
34+
expires_at timestamptz not null,
35+
primary key (id),
36+
foreign key (tenant_id, inviter_id, organization_id)
37+
references organization_user_relations (tenant_id, user_id, organization_id)
38+
on update cascade on delete cascade
39+
);
40+
`);
41+
await applyTableRls(pool, 'organization_invitations');
42+
43+
await pool.query(sql`
44+
create table organization_invitation_roles (
45+
tenant_id varchar(21) not null
46+
references tenants (id) on update cascade on delete cascade,
47+
/** The ID of the invitation. */
48+
invitation_id varchar(21) not null
49+
references organization_invitations (id) on update cascade on delete cascade,
50+
/** The ID of the organization role. */
51+
organization_role_id varchar(21) not null
52+
references organization_roles (id) on update cascade on delete cascade,
53+
primary key (tenant_id, invitation_id, organization_role_id)
54+
);
55+
`);
56+
await applyTableRls(pool, 'organization_invitation_roles');
57+
},
58+
down: async (pool) => {
59+
await dropTableRls(pool, 'organization_invitations');
60+
await pool.query(sql`
61+
drop table organization_invitations;
62+
`);
63+
await dropTableRls(pool, 'organization_invitation_roles');
64+
await pool.query(sql`
65+
drop table organization_invitation_roles;
66+
`);
67+
},
68+
};
69+
70+
export default alteration;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { type CommonQueryMethods, sql } from 'slonik';
2+
3+
const getId = (value: string) => sql.identifier([value]);
4+
5+
const getDatabaseName = async (pool: CommonQueryMethods) => {
6+
const { currentDatabase } = await pool.one<{ currentDatabase: string }>(sql`
7+
select current_database();
8+
`);
9+
10+
return currentDatabase.replaceAll('-', '_');
11+
};
12+
13+
/**
14+
* A function to call after the table is created. It will apply the necessary row-level security
15+
* policies and triggers to the table.
16+
*/
17+
export const applyTableRls = async (pool: CommonQueryMethods, tableName: string) => {
18+
const database = await getDatabaseName(pool);
19+
const baseRoleId = getId(`logto_tenant_${database}`);
20+
const table = getId(tableName);
21+
22+
await pool.query(sql`
23+
create trigger set_tenant_id before insert on ${table}
24+
for each row execute procedure set_tenant_id();
25+
26+
alter table ${table} enable row level security;
27+
28+
create policy ${getId(`${tableName}_tenant_id`)} on ${table}
29+
as restrictive
30+
using (tenant_id = (select id from tenants where db_user = current_user));
31+
32+
create policy ${getId(`${tableName}_modification`)} on ${table}
33+
using (true);
34+
35+
grant select, insert, update, delete on ${table} to ${baseRoleId};
36+
`);
37+
};
38+
39+
/**
40+
* A function to call before the table is dropped. It will remove the row-level security policies
41+
* and triggers from the table.
42+
*/
43+
export const dropTableRls = async (pool: CommonQueryMethods, tableName: string) => {
44+
await pool.query(sql`
45+
drop policy ${getId(`${tableName}_modification`)} on ${getId(tableName)};
46+
drop policy ${getId(`${tableName}_tenant_id`)} on ${getId(tableName)};
47+
drop trigger set_tenant_id on ${getId(tableName)};
48+
`);
49+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Alteration utils
2+
3+
This directory contains utilities for database alteration scripts.
4+
5+
Due to the nature of alteration, all utility functions should be maintained in an immutable way. This means when a function needs to be changed, a new file should be created with the following name format:
6+
7+
`<timestamp>-<function-or-purpose>.js`
8+
9+
The timestamp should be in the format of epoch time in seconds. The original file should be kept for historical purposes.

packages/schemas/src/foundations/jsonb-types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from './sentinel.js';
1111
export * from './users.js';
1212
export * from './sso-connector.js';
1313
export * from './applications.js';
14+
export * from './organizations.js';
1415

1516
export {
1617
configurableConnectorMetadataGuard,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { z } from 'zod';
2+
3+
/** The status of an organization invitation. */
4+
export enum OrganizationInvitationStatus {
5+
/** The invitation is pending for the invitee's response. */
6+
Pending = 'Pending',
7+
/** The invitation is accepted by the invitee. */
8+
Accepted = 'Accepted',
9+
/** The invitation is revoked by the inviter. */
10+
Revoked = 'Revoked',
11+
/** The invitation is expired, or the invitee has already joined the organization. */
12+
Expired = 'Expired',
13+
}
14+
export const organizationInvitationStatusGuard = z.nativeEnum(OrganizationInvitationStatus);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/* init_order = 1 */
2+
3+
/**
4+
* Link that can be used to perform certain actions by verifying the token. The expiration time
5+
* of the link should be determined by the action it performs, thus there is no `expires_at`
6+
* column in this table.
7+
*/
8+
create table magic_links (
9+
tenant_id varchar(21) not null
10+
references tenants (id) on update cascade on delete cascade,
11+
/** The unique identifier of the link. */
12+
id varchar(21) not null,
13+
/** The token that can be used to verify the link. */
14+
token varchar(32) not null,
15+
/** The time when the link was created. */
16+
created_at timestamptz not null default (now()),
17+
/** The time when the link was consumed. */
18+
consumed_at timestamptz,
19+
primary key (id)
20+
);
21+
22+
create index magic_links__token
23+
on magic_links (tenant_id, token);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/* init_order = 4 */
2+
3+
/** The organization roles that will be assigned to a user when they accept an invitation. */
4+
create table organization_invitation_roles (
5+
tenant_id varchar(21) not null
6+
references tenants (id) on update cascade on delete cascade,
7+
/** The ID of the invitation. */
8+
invitation_id varchar(21) not null
9+
references organization_invitations (id) on update cascade on delete cascade,
10+
/** The ID of the organization role. */
11+
organization_role_id varchar(21) not null
12+
references organization_roles (id) on update cascade on delete cascade,
13+
primary key (tenant_id, invitation_id, organization_role_id)
14+
);

0 commit comments

Comments
 (0)