@logto/schemas 1.34.0 → 1.36.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.
Files changed (89) hide show
  1. package/alterations/1.35.0-1764580455-remove-daily-active-users-foreign-key.ts +69 -0
  2. package/alterations/1.35.0-1764580589-create-aggregated-daily-active-users-table.ts +51 -0
  3. package/alterations/1.35.0-1764653048-update-daily-token-usage-mau-support.ts +37 -0
  4. package/alterations/1.35.0-1765183934-add-logs-created-at-id-index.ts +39 -0
  5. package/alterations/1.35.0-1765255453-update-saml-session-relay-state-to-varchar-512.ts +31 -0
  6. package/alterations/1.35.0-1765631949-drop-redundant-logs-id-index.ts +47 -0
  7. package/alterations/1.35.0-1766028646-grant-tenants-table-tag-column-read-permission.ts +39 -0
  8. package/alterations/1.36.0-1767193412-allow-token-exchange.ts +34 -0
  9. package/alterations/1.36.0-1767859553-passkey-sign-in.ts +21 -0
  10. package/alterations/1.36.0-1768192304-enable-account-center-for-admin-tenant.ts +32 -0
  11. package/alterations/1.36.0-1768464306-enable-mfa-for-admin-tenant.ts +30 -0
  12. package/alterations/1.36.0-1768758295-add-user-geo-location.ts +32 -0
  13. package/alterations/1.36.0-1768891516-add-user-sign-in-countries-table.ts +33 -0
  14. package/alterations/1.36.0-1769067642-add-adaptive-mfa-configuration.ts +19 -0
  15. package/alterations/1.36.0-1769172677-enable-organization-mfa-policy-for-admin-tenant.ts +31 -0
  16. package/alterations-js/1.35.0-1764580455-remove-daily-active-users-foreign-key.js +57 -0
  17. package/alterations-js/1.35.0-1764580589-create-aggregated-daily-active-users-table.js +40 -0
  18. package/alterations-js/1.35.0-1764653048-update-daily-token-usage-mau-support.js +31 -0
  19. package/alterations-js/1.35.0-1765183934-add-logs-created-at-id-index.js +35 -0
  20. package/alterations-js/1.35.0-1765255453-update-saml-session-relay-state-to-varchar-512.js +25 -0
  21. package/alterations-js/1.35.0-1765631949-drop-redundant-logs-id-index.js +43 -0
  22. package/alterations-js/1.35.0-1766028646-grant-tenants-table-tag-column-read-permission.js +31 -0
  23. package/alterations-js/1.36.0-1767193412-allow-token-exchange.js +30 -0
  24. package/alterations-js/1.36.0-1767859553-passkey-sign-in.js +17 -0
  25. package/alterations-js/1.36.0-1768192304-enable-account-center-for-admin-tenant.js +27 -0
  26. package/alterations-js/1.36.0-1768464306-enable-mfa-for-admin-tenant.js +25 -0
  27. package/alterations-js/1.36.0-1768758295-add-user-geo-location.js +27 -0
  28. package/alterations-js/1.36.0-1768891516-add-user-sign-in-countries-table.js +28 -0
  29. package/alterations-js/1.36.0-1769067642-add-adaptive-mfa-configuration.js +15 -0
  30. package/alterations-js/1.36.0-1769172677-enable-organization-mfa-policy-for-admin-tenant.js +26 -0
  31. package/lib/consts/product-event.d.ts +0 -12
  32. package/lib/consts/product-event.js +0 -13
  33. package/lib/db-entries/aggregated-daily-active-user.d.ts +22 -0
  34. package/lib/db-entries/aggregated-daily-active-user.js +33 -0
  35. package/lib/db-entries/daily-token-usage.d.ts +5 -1
  36. package/lib/db-entries/daily-token-usage.js +8 -0
  37. package/lib/db-entries/index.d.ts +3 -0
  38. package/lib/db-entries/index.js +3 -0
  39. package/lib/db-entries/saml-application-session.js +2 -2
  40. package/lib/db-entries/sign-in-experience.d.ts +6 -2
  41. package/lib/db-entries/sign-in-experience.js +9 -1
  42. package/lib/db-entries/user-geo-location.d.ts +24 -0
  43. package/lib/db-entries/user-geo-location.js +37 -0
  44. package/lib/db-entries/user-sign-in-country.d.ts +24 -0
  45. package/lib/db-entries/user-sign-in-country.js +33 -0
  46. package/lib/foundations/jsonb-types/account-centers.d.ts +2 -2
  47. package/lib/foundations/jsonb-types/captcha.d.ts +16 -0
  48. package/lib/foundations/jsonb-types/captcha.js +7 -0
  49. package/lib/foundations/jsonb-types/logs.d.ts +703 -0
  50. package/lib/foundations/jsonb-types/logs.js +52 -0
  51. package/lib/foundations/jsonb-types/oidc-module.d.ts +15 -3
  52. package/lib/foundations/jsonb-types/oidc-module.js +15 -3
  53. package/lib/foundations/jsonb-types/saml-application-configs.d.ts +1 -1
  54. package/lib/foundations/jsonb-types/sentinel.d.ts +13 -1
  55. package/lib/foundations/jsonb-types/sentinel.js +12 -0
  56. package/lib/foundations/jsonb-types/sign-in-experience.d.ts +59 -0
  57. package/lib/foundations/jsonb-types/sign-in-experience.js +11 -0
  58. package/lib/seeds/account-center.d.ts +6 -0
  59. package/lib/seeds/account-center.js +24 -0
  60. package/lib/seeds/cloud-api.d.ts +3 -1
  61. package/lib/seeds/cloud-api.js +2 -0
  62. package/lib/seeds/sign-in-experience.js +6 -1
  63. package/lib/types/alteration.d.ts +11 -1
  64. package/lib/types/application.d.ts +6 -0
  65. package/lib/types/consent.d.ts +4 -0
  66. package/lib/types/custom-profile-fields.d.ts +3 -3
  67. package/lib/types/hook.d.ts +2 -2
  68. package/lib/types/interactions.d.ts +16 -7
  69. package/lib/types/interactions.js +10 -4
  70. package/lib/types/log/index.d.ts +12 -6
  71. package/lib/types/log/interaction.d.ts +5 -1
  72. package/lib/types/logto-config/index.d.ts +9 -9
  73. package/lib/types/logto-config/jwt-customizer.d.ts +17 -17
  74. package/lib/types/saml-application.d.ts +7 -7
  75. package/lib/types/sign-in-experience.d.ts +19 -1
  76. package/lib/types/sign-in-experience.js +3 -1
  77. package/lib/types/user.d.ts +10 -7
  78. package/lib/types/user.js +1 -0
  79. package/package.json +6 -6
  80. package/tables/_after_all.sql +1 -1
  81. package/tables/aggregated_daily_active_users.sql +16 -0
  82. package/tables/daily_active_users.sql +9 -4
  83. package/tables/daily_token_usage.sql +3 -2
  84. package/tables/logs.sql +3 -3
  85. package/tables/saml_application_sessions.sql +1 -1
  86. package/tables/sign_in_experiences.sql +2 -0
  87. package/tables/user_geo_locations.sql +14 -0
  88. package/tables/user_sign_in_countries.sql +16 -0
  89. package/tables/users.sql +3 -0
@@ -0,0 +1,69 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ /**
6
+ * Remove foreign key constraint from daily_active_users table to allow
7
+ * historical billing data to persist even after tenant deletion.
8
+ * This supports the MAU-based billing system requirements.
9
+ *
10
+ * Index optimizations:
11
+ * 1. Removes redundant (tenant_id, id) index (id is already primary key)
12
+ * 2. Adds optimized index (tenant_id, date, user_id) for aggregation queries
13
+ * 3. Replaces problematic partial index with BRIN index
14
+ */
15
+
16
+ const alteration: AlterationScript = {
17
+ up: async (pool) => {
18
+ // Drop the existing foreign key constraint
19
+ await pool.query(sql`
20
+ alter table daily_active_users
21
+ drop constraint if exists daily_active_users_tenant_id_fkey
22
+ `);
23
+
24
+ // Remove the redundant (tenant_id, id) index since id is already primary key
25
+ await pool.query(sql`
26
+ drop index if exists daily_active_users__id
27
+ `);
28
+
29
+ // Add optimized index for aggregation queries with better write performance
30
+ await pool.query(sql`
31
+ create index daily_active_users__tenant_date_user
32
+ on daily_active_users (tenant_id, date, user_id)
33
+ `);
34
+
35
+ // Add BRIN index for time-series date range queries
36
+ // Optimized for sequential data insertion and range scans (date >= ?)
37
+ await pool.query(sql`
38
+ create index daily_active_users__date_brin
39
+ on daily_active_users using brin (date)
40
+ `);
41
+ },
42
+
43
+ down: async (pool) => {
44
+ // Drop the new indexes we created
45
+ await pool.query(sql`
46
+ drop index if exists daily_active_users__date_brin
47
+ `);
48
+
49
+ await pool.query(sql`
50
+ drop index if exists daily_active_users__tenant_date_user
51
+ `);
52
+
53
+ // Recreate the original redundant index
54
+ await pool.query(sql`
55
+ create index daily_active_users__id
56
+ on daily_active_users (tenant_id, id)
57
+ `);
58
+
59
+ // Recreate the foreign key constraint
60
+ await pool.query(sql`
61
+ alter table daily_active_users
62
+ add constraint daily_active_users_tenant_id_fkey
63
+ foreign key (tenant_id) references tenants(id)
64
+ on update cascade on delete cascade
65
+ `);
66
+ },
67
+ };
68
+
69
+ export default alteration;
@@ -0,0 +1,51 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ import { applyTableRls, dropTableRls } from './utils/1704934999-tables.js';
6
+
7
+ /**
8
+ * Create aggregated_daily_active_users table for MAU-based billing system.
9
+ * This table consolidates daily user activities for efficient billing calculations.
10
+ */
11
+
12
+ const alteration: AlterationScript = {
13
+ up: async (pool) => {
14
+ // Create the aggregated daily active users table
15
+ await pool.query(sql`
16
+ create table aggregated_daily_active_users (
17
+ tenant_id varchar(21) not null,
18
+ activity_date date not null,
19
+ user_id varchar(21) not null,
20
+ activity_count integer not null,
21
+ primary key (tenant_id, activity_date, user_id)
22
+ );
23
+ `);
24
+
25
+ // Index for billing cycle range queries
26
+ await pool.query(sql`
27
+ create index aggregated_daily_active_users__tenant_date
28
+ on aggregated_daily_active_users (tenant_id, activity_date);
29
+ `);
30
+
31
+ // Index for tenant-specific user activity queries
32
+ await pool.query(sql`
33
+ create index aggregated_daily_active_users__tenant_user_date
34
+ on aggregated_daily_active_users (tenant_id, user_id, activity_date desc);
35
+ `);
36
+
37
+ await applyTableRls(pool, 'aggregated_daily_active_users');
38
+ },
39
+
40
+ down: async (pool) => {
41
+ // Drop RLS policies first
42
+ await dropTableRls(pool, 'aggregated_daily_active_users');
43
+
44
+ // Drop the table and all its indexes
45
+ await pool.query(sql`
46
+ drop table aggregated_daily_active_users;
47
+ `);
48
+ },
49
+ };
50
+
51
+ export default alteration;
@@ -0,0 +1,37 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ const alteration: AlterationScript = {
6
+ up: async (pool) => {
7
+ // Add new columns for user and m2m token usage tracking
8
+ await pool.query(sql`
9
+ alter table daily_token_usage
10
+ add column user_token_usage bigint not null default 0,
11
+ add column m2m_token_usage bigint not null default 0;
12
+ `);
13
+
14
+ // Remove foreign key constraint to support enterprise tenant deletion requirements
15
+ await pool.query(sql`
16
+ alter table daily_token_usage
17
+ drop constraint if exists daily_token_usage_tenant_id_fkey;
18
+ `);
19
+ },
20
+ down: async (pool) => {
21
+ // Remove the new columns
22
+ await pool.query(sql`
23
+ alter table daily_token_usage
24
+ drop column if exists user_token_usage,
25
+ drop column if exists m2m_token_usage;
26
+ `);
27
+
28
+ // Re-add the foreign key constraint
29
+ await pool.query(sql`
30
+ alter table daily_token_usage
31
+ add constraint daily_token_usage_tenant_id_fkey
32
+ foreign key (tenant_id) references tenants (id) on update cascade on delete cascade;
33
+ `);
34
+ },
35
+ };
36
+
37
+ export default alteration;
@@ -0,0 +1,39 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ const alteration: AlterationScript = {
6
+ beforeUp: async (pool) => {
7
+ /**
8
+ * Use 'if not exists' to ensure idempotency.
9
+ * If the subsequent transaction in 'up' fails, the migration can be safely re-run without failing on an already created index.
10
+ */
11
+ await pool.query(sql`
12
+ create index concurrently if not exists logs__created_at_id
13
+ on logs (tenant_id, created_at, id);
14
+ `);
15
+ },
16
+ up: async () => {
17
+ /**
18
+ * The index on the logs table must be created outside of a transaction to avoid table locks.
19
+ * 'concurrently' cannot be used inside a transaction, so this up is intentionally left empty.
20
+ */
21
+ },
22
+ beforeDown: async (pool) => {
23
+ /**
24
+ * Use 'if exists' to ensure idempotency. If the subsequent transaction in 'down' fails,
25
+ * the rollback can be safely re-run without failing on a non-existent index.
26
+ */
27
+ await pool.query(sql`
28
+ drop index concurrently if exists logs__created_at_id;
29
+ `);
30
+ },
31
+ down: async () => {
32
+ /**
33
+ * The index on the logs table must be dropped outside of a transaction to avoid table locks.
34
+ * 'concurrently' cannot be used inside a transaction, so this down is intentionally left empty.
35
+ */
36
+ },
37
+ };
38
+
39
+ export default alteration;
@@ -0,0 +1,31 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ /**
6
+ * Update saml_application_sessions.relay_state column type from varchar(256) to text
7
+ * to support longer relay state values that may be used in SAML authentication flows.
8
+ *
9
+ * The relay state parameter in SAML can contain arbitrary data that needs to be
10
+ * preserved and returned to the service provider, and 256 characters may not be
11
+ * sufficient for all use cases.
12
+ */
13
+
14
+ const alteration: AlterationScript = {
15
+ up: async (pool) => {
16
+ await pool.query(sql`
17
+ alter table saml_application_sessions
18
+ alter column relay_state type varchar(512)
19
+ `);
20
+ },
21
+
22
+ down: async (pool) => {
23
+ // The down migration may fail or cause data loss if any existing relay_state values exceed 256 characters. Consider adding a USING clause to safely truncate values during rollback, this ensures the rollback operation won't fail due to data that's too long for the varchar(256) constraint.
24
+ await pool.query(sql`
25
+ alter table saml_application_sessions
26
+ alter column relay_state type varchar(256) using left(relay_state, 256)
27
+ `);
28
+ },
29
+ };
30
+
31
+ export default alteration;
@@ -0,0 +1,47 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ /**
6
+ * The `logs__id` index on (tenant_id, id) is redundant because:
7
+ * 1. `id` is already the primary key, which can efficiently lookup by id
8
+ * 2. `logs__created_at_id` index on (tenant_id, created_at, id) already covers tenant_id queries
9
+ *
10
+ * Removing this index reduces write IOPS during log deletion operations,
11
+ * as each DELETE no longer needs to update this redundant index.
12
+ */
13
+ const alteration: AlterationScript = {
14
+ beforeUp: async (pool) => {
15
+ /**
16
+ * Use 'if exists' to ensure idempotency.
17
+ * 'concurrently' avoids table locks during index drop.
18
+ */
19
+ await pool.query(sql`
20
+ drop index concurrently if exists logs__id;
21
+ `);
22
+ },
23
+ up: async () => {
24
+ /**
25
+ * The index must be dropped outside of a transaction to avoid table locks.
26
+ * 'concurrently' cannot be used inside a transaction, so this up is intentionally left empty.
27
+ */
28
+ },
29
+ beforeDown: async (pool) => {
30
+ /**
31
+ * Recreate the index if rolling back.
32
+ * Use 'if not exists' to ensure idempotency.
33
+ */
34
+ await pool.query(sql`
35
+ create index concurrently if not exists logs__id
36
+ on logs (tenant_id, id);
37
+ `);
38
+ },
39
+ down: async () => {
40
+ /**
41
+ * The index must be created outside of a transaction to avoid table locks.
42
+ * 'concurrently' cannot be used inside a transaction, so this down is intentionally left empty.
43
+ */
44
+ },
45
+ };
46
+
47
+ export default alteration;
@@ -0,0 +1,39 @@
1
+ import { type CommonQueryMethods, sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
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
+ * Grant read permission to the tag column in the tenants table to the logto_tenant_<databaseName> role.
15
+ */
16
+ const alteration: AlterationScript = {
17
+ up: async (pool) => {
18
+ const databaseName = await getDatabaseName(pool);
19
+ const baseRoleId = sql.identifier([`logto_tenant_${databaseName}`]);
20
+
21
+ await pool.query(sql`
22
+ grant select (tag)
23
+ on table tenants
24
+ to ${baseRoleId}
25
+ `);
26
+ },
27
+ down: async (pool) => {
28
+ const databaseName = await getDatabaseName(pool);
29
+ const baseRoleId = sql.identifier([`logto_tenant_${databaseName}`]);
30
+
31
+ await pool.query(sql`
32
+ revoke select (tag)
33
+ on table tenants
34
+ from ${baseRoleId}
35
+ `);
36
+ },
37
+ };
38
+
39
+ export default alteration;
@@ -0,0 +1,34 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ const alteration: AlterationScript = {
6
+ up: async (pool) => {
7
+ /**
8
+ * For backward compatibility, set allowTokenExchange = true for existing first-party
9
+ * Traditional, Native, and SPA applications.
10
+ * M2M applications were never allowed to use token exchange before this feature.
11
+ *
12
+ * The admin-console should keep token exchange disabled, so we skip it here.
13
+ */
14
+ await pool.query(sql`
15
+ update applications
16
+ set custom_client_metadata = custom_client_metadata || '{"allowTokenExchange": true}'::jsonb
17
+ where is_third_party = false
18
+ and type in ('Traditional', 'Native', 'SPA')
19
+ and id != 'admin-console';
20
+ `);
21
+ },
22
+ down: async (pool) => {
23
+ // Remove allowTokenExchange only from applications that were updated in the `up` migration
24
+ await pool.query(sql`
25
+ update applications
26
+ set custom_client_metadata = custom_client_metadata - 'allowTokenExchange'
27
+ where is_third_party = false
28
+ and type in ('Traditional', 'Native', 'SPA')
29
+ and id != 'admin-console';
30
+ `);
31
+ },
32
+ };
33
+
34
+ export default alteration;
@@ -0,0 +1,21 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ const alteration: AlterationScript = {
6
+ up: async (pool) => {
7
+ await pool.query(sql`
8
+ alter table sign_in_experiences
9
+ add column passkey_sign_in jsonb /* @use PasskeySignIn */ not null default '{}'::jsonb;
10
+ create index users_mfa_verifications_gin on users using gin (mfa_verifications jsonb_path_ops);
11
+ `);
12
+ },
13
+ down: async (pool) => {
14
+ await pool.query(sql`
15
+ alter table sign_in_experiences drop column passkey_sign_in;
16
+ drop index users_mfa_verifications_gin;
17
+ `);
18
+ },
19
+ };
20
+
21
+ export default alteration;
@@ -0,0 +1,32 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ const adminTenantId = 'admin';
6
+
7
+ /**
8
+ * Enable the account center for the admin tenant and set all fields to Edit.
9
+ * This allows the console profile page to use the Account API.
10
+ */
11
+ const alteration: AlterationScript = {
12
+ up: async (pool) => {
13
+ await pool.query(sql`
14
+ update account_centers
15
+ set enabled = true,
16
+ fields = '{"name": "Edit", "avatar": "Edit", "profile": "Edit", "email": "Edit", "phone": "Edit", "password": "Edit", "username": "Edit", "social": "Edit", "customData": "Edit", "mfa": "Edit"}'::jsonb
17
+ where tenant_id = ${adminTenantId}
18
+ and id = 'default'
19
+ `);
20
+ },
21
+ down: async (pool) => {
22
+ await pool.query(sql`
23
+ update account_centers
24
+ set enabled = false,
25
+ fields = '{}'::jsonb
26
+ where tenant_id = ${adminTenantId}
27
+ and id = 'default'
28
+ `);
29
+ },
30
+ };
31
+
32
+ export default alteration;
@@ -0,0 +1,30 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ const adminTenantId = 'admin';
6
+
7
+ /**
8
+ * Enable MFA (TOTP and WebAuthn) for the admin tenant with NoPrompt policy.
9
+ * This allows users to optionally set up MFA via the account center.
10
+ */
11
+ const alteration: AlterationScript = {
12
+ up: async (pool) => {
13
+ await pool.query(sql`
14
+ update sign_in_experiences
15
+ set mfa = '{"factors":["Totp","WebAuthn","BackupCode"],"policy":"NoPrompt"}'::jsonb
16
+ where tenant_id = ${adminTenantId}
17
+ and id = 'default'
18
+ `);
19
+ },
20
+ down: async (pool) => {
21
+ await pool.query(sql`
22
+ update sign_in_experiences
23
+ set mfa = '{"factors":[],"policy":"UserControlled"}'::jsonb
24
+ where tenant_id = ${adminTenantId}
25
+ and id = 'default'
26
+ `);
27
+ },
28
+ };
29
+
30
+ export default alteration;
@@ -0,0 +1,32 @@
1
+ import { sql } from '@silverhand/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 user_geo_locations (
11
+ tenant_id varchar(21) not null
12
+ references tenants (id) on update cascade on delete cascade,
13
+ user_id varchar(12) not null
14
+ references users (id) on update cascade on delete cascade,
15
+ latitude numeric(9,6),
16
+ longitude numeric(9,6),
17
+ updated_at timestamptz not null default now(),
18
+ primary key (tenant_id, user_id),
19
+ check ((latitude is null) = (longitude is null))
20
+ );
21
+ `);
22
+ await applyTableRls(pool, 'user_geo_locations');
23
+ },
24
+ down: async (pool) => {
25
+ await dropTableRls(pool, 'user_geo_locations');
26
+ await pool.query(sql`
27
+ drop table user_geo_locations;
28
+ `);
29
+ },
30
+ };
31
+
32
+ export default alteration;
@@ -0,0 +1,33 @@
1
+ import { sql } from '@silverhand/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 user_sign_in_countries (
11
+ tenant_id varchar(21) not null
12
+ references tenants (id) on update cascade on delete cascade,
13
+ user_id varchar(12) not null
14
+ references users (id) on update cascade on delete cascade,
15
+ country varchar(16) not null,
16
+ last_sign_in_at timestamptz not null default(now()),
17
+ primary key (tenant_id, user_id, country)
18
+ );
19
+
20
+ create index user_sign_in_countries__tenant_user_last_sign_in_at
21
+ on user_sign_in_countries (tenant_id, user_id, last_sign_in_at desc);
22
+ `);
23
+ await applyTableRls(pool, 'user_sign_in_countries');
24
+ },
25
+ down: async (pool) => {
26
+ await dropTableRls(pool, 'user_sign_in_countries');
27
+ await pool.query(sql`
28
+ drop table user_sign_in_countries;
29
+ `);
30
+ },
31
+ };
32
+
33
+ export default alteration;
@@ -0,0 +1,19 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ const alteration: AlterationScript = {
6
+ up: async (pool) => {
7
+ await pool.query(sql`
8
+ alter table sign_in_experiences
9
+ add column adaptive_mfa jsonb not null default '{}'::jsonb;
10
+ `);
11
+ },
12
+ down: async (pool) => {
13
+ await pool.query(sql`
14
+ alter table sign_in_experiences drop column adaptive_mfa;
15
+ `);
16
+ },
17
+ };
18
+
19
+ export default alteration;
@@ -0,0 +1,31 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ const adminTenantId = 'admin';
6
+
7
+ /**
8
+ * Enable organization required MFA policy for the admin tenant.
9
+ * This allows tenant admins to require MFA for all tenant members
10
+ * by setting `isMfaRequired: true` on the organization.
11
+ */
12
+ const alteration: AlterationScript = {
13
+ up: async (pool) => {
14
+ await pool.query(sql`
15
+ update sign_in_experiences
16
+ set mfa = jsonb_set(mfa, '{organizationRequiredMfaPolicy}', '"Mandatory"')
17
+ where tenant_id = ${adminTenantId}
18
+ and id = 'default'
19
+ `);
20
+ },
21
+ down: async (pool) => {
22
+ await pool.query(sql`
23
+ update sign_in_experiences
24
+ set mfa = mfa - 'organizationRequiredMfaPolicy'
25
+ where tenant_id = ${adminTenantId}
26
+ and id = 'default'
27
+ `);
28
+ },
29
+ };
30
+
31
+ export default alteration;
@@ -0,0 +1,57 @@
1
+ import { sql } from '@silverhand/slonik';
2
+ /**
3
+ * Remove foreign key constraint from daily_active_users table to allow
4
+ * historical billing data to persist even after tenant deletion.
5
+ * This supports the MAU-based billing system requirements.
6
+ *
7
+ * Index optimizations:
8
+ * 1. Removes redundant (tenant_id, id) index (id is already primary key)
9
+ * 2. Adds optimized index (tenant_id, date, user_id) for aggregation queries
10
+ * 3. Replaces problematic partial index with BRIN index
11
+ */
12
+ const alteration = {
13
+ up: async (pool) => {
14
+ // Drop the existing foreign key constraint
15
+ await pool.query(sql `
16
+ alter table daily_active_users
17
+ drop constraint if exists daily_active_users_tenant_id_fkey
18
+ `);
19
+ // Remove the redundant (tenant_id, id) index since id is already primary key
20
+ await pool.query(sql `
21
+ drop index if exists daily_active_users__id
22
+ `);
23
+ // Add optimized index for aggregation queries with better write performance
24
+ await pool.query(sql `
25
+ create index daily_active_users__tenant_date_user
26
+ on daily_active_users (tenant_id, date, user_id)
27
+ `);
28
+ // Add BRIN index for time-series date range queries
29
+ // Optimized for sequential data insertion and range scans (date >= ?)
30
+ await pool.query(sql `
31
+ create index daily_active_users__date_brin
32
+ on daily_active_users using brin (date)
33
+ `);
34
+ },
35
+ down: async (pool) => {
36
+ // Drop the new indexes we created
37
+ await pool.query(sql `
38
+ drop index if exists daily_active_users__date_brin
39
+ `);
40
+ await pool.query(sql `
41
+ drop index if exists daily_active_users__tenant_date_user
42
+ `);
43
+ // Recreate the original redundant index
44
+ await pool.query(sql `
45
+ create index daily_active_users__id
46
+ on daily_active_users (tenant_id, id)
47
+ `);
48
+ // Recreate the foreign key constraint
49
+ await pool.query(sql `
50
+ alter table daily_active_users
51
+ add constraint daily_active_users_tenant_id_fkey
52
+ foreign key (tenant_id) references tenants(id)
53
+ on update cascade on delete cascade
54
+ `);
55
+ },
56
+ };
57
+ export default alteration;
@@ -0,0 +1,40 @@
1
+ import { sql } from '@silverhand/slonik';
2
+ import { applyTableRls, dropTableRls } from './utils/1704934999-tables.js';
3
+ /**
4
+ * Create aggregated_daily_active_users table for MAU-based billing system.
5
+ * This table consolidates daily user activities for efficient billing calculations.
6
+ */
7
+ const alteration = {
8
+ up: async (pool) => {
9
+ // Create the aggregated daily active users table
10
+ await pool.query(sql `
11
+ create table aggregated_daily_active_users (
12
+ tenant_id varchar(21) not null,
13
+ activity_date date not null,
14
+ user_id varchar(21) not null,
15
+ activity_count integer not null,
16
+ primary key (tenant_id, activity_date, user_id)
17
+ );
18
+ `);
19
+ // Index for billing cycle range queries
20
+ await pool.query(sql `
21
+ create index aggregated_daily_active_users__tenant_date
22
+ on aggregated_daily_active_users (tenant_id, activity_date);
23
+ `);
24
+ // Index for tenant-specific user activity queries
25
+ await pool.query(sql `
26
+ create index aggregated_daily_active_users__tenant_user_date
27
+ on aggregated_daily_active_users (tenant_id, user_id, activity_date desc);
28
+ `);
29
+ await applyTableRls(pool, 'aggregated_daily_active_users');
30
+ },
31
+ down: async (pool) => {
32
+ // Drop RLS policies first
33
+ await dropTableRls(pool, 'aggregated_daily_active_users');
34
+ // Drop the table and all its indexes
35
+ await pool.query(sql `
36
+ drop table aggregated_daily_active_users;
37
+ `);
38
+ },
39
+ };
40
+ export default alteration;