mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-11 07:45:50 +00:00
feat: add team columns to group and jct_user_group
Schema foundations for teams. Ships dark — nothing reads these columns yet. group: kind, name, handle, plan_id, deleted_at jct_user_group: org_owned user: requires_password_change `kind = 'team'` marks a workspace; the seeded system groups keep it NULL so a team query can never surface them. `org_owned` distinguishes accounts the workspace created from the master account, deciding who pays rather than who may read. `requires_password_change` is a fourth `requires_*` flag for assertVerifiedAccount, needed by the phase 5 reset flow but shipped here to avoid a second three-dialect migration. `handle` uniqueness is case-insensitive, and the dialects disagree by default: mysql gets it from utf8mb4_unicode_ci, sqlite needs COLLATE NOCASE (as 0055 does for usernames), and postgres indexes lower(handle). Without this the same migration would accept `Design-Team` next to `design-team` on sqlite and postgres while mysql rejected it. Postgres handle lookups must therefore compare lower(handle) to use the index. idx_group_owner is sqlite-only: mysql and postgres already index that column. idx_jct_user_group_group is composite, unlike the existing single-column keys.
This commit is contained in:
@@ -27,7 +27,7 @@ import { DatabaseClientFactory } from './index.js';
|
||||
import { SqliteDatabaseClient } from './SqliteDatabaseClient.js';
|
||||
|
||||
/** Highest schema version the migration table can reach. */
|
||||
const CURRENT_SCHEMA_VERSION = 72;
|
||||
const CURRENT_SCHEMA_VERSION = 73;
|
||||
|
||||
/**
|
||||
* These suites migrate real files on disk. Idle they finish in well under a
|
||||
@@ -81,6 +81,69 @@ describe('SqliteDatabaseClient — boot and migrations', { timeout: DISK_MIGRATI
|
||||
expect(await userVersionOf(client)).toBe(CURRENT_SCHEMA_VERSION);
|
||||
});
|
||||
|
||||
it('applies the team columns and indexes from 0077', async () => {
|
||||
const columnsOf = async (table: string) =>
|
||||
(
|
||||
(await client.read(
|
||||
`SELECT name FROM pragma_table_info('${table}')`,
|
||||
)) as { name: string }[]
|
||||
).map((r) => r.name);
|
||||
|
||||
expect(await columnsOf('group')).toEqual(
|
||||
expect.arrayContaining([
|
||||
'kind',
|
||||
'name',
|
||||
'handle',
|
||||
'deleted_at',
|
||||
]),
|
||||
);
|
||||
expect(await columnsOf('jct_user_group')).toContain('org_owned');
|
||||
expect(await columnsOf('user')).toContain('requires_password_change');
|
||||
|
||||
const indexes = (await client.read(
|
||||
"SELECT name FROM sqlite_master WHERE type = 'index' AND name IN (?, ?, ?)",
|
||||
['idx_group_handle', 'idx_group_owner', 'idx_jct_user_group_group'],
|
||||
)) as { name: string }[];
|
||||
expect(indexes.map((r) => r.name).sort()).toEqual([
|
||||
'idx_group_handle',
|
||||
'idx_group_owner',
|
||||
'idx_jct_user_group_group',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps `handle` unique but lets the seeded groups share a NULL one', async () => {
|
||||
const seeded = (await client.read(
|
||||
'SELECT COUNT(*) AS n FROM `group` WHERE `handle` IS NULL',
|
||||
)) as { n: number }[];
|
||||
expect(seeded[0].n).toBeGreaterThan(1);
|
||||
|
||||
await client.write(
|
||||
'UPDATE `group` SET `handle` = ? WHERE `id` = (SELECT MIN(`id`) FROM `group`)',
|
||||
['taken'],
|
||||
);
|
||||
await expect(
|
||||
client.write(
|
||||
'UPDATE `group` SET `handle` = ? WHERE `id` = (SELECT MAX(`id`) FROM `group`)',
|
||||
['taken'],
|
||||
),
|
||||
).rejects.toThrow(/UNIQUE/iu);
|
||||
});
|
||||
|
||||
it('treats `handle` case-insensitively, as usernames are treated', async () => {
|
||||
await client.write(
|
||||
'UPDATE `group` SET `handle` = ? WHERE `id` = (SELECT MIN(`id`) FROM `group`)',
|
||||
['design-team'],
|
||||
);
|
||||
// Without COLLATE NOCASE this differs per engine: MySQL rejects it via
|
||||
// utf8mb4_unicode_ci while sqlite and postgres would let it through.
|
||||
await expect(
|
||||
client.write(
|
||||
'UPDATE `group` SET `handle` = ? WHERE `id` = (SELECT MAX(`id`) FROM `group`)',
|
||||
['Design-Team'],
|
||||
),
|
||||
).rejects.toThrow(/UNIQUE/iu);
|
||||
});
|
||||
|
||||
it('runs the javascript migrations, not just the .sql ones', async () => {
|
||||
// The `system` user only exists because 0025 (a .dbmig.js file) ran
|
||||
// inside the migration VM.
|
||||
|
||||
@@ -106,6 +106,7 @@ const AVAILABLE_MIGRATIONS: [number, string[]][] = [
|
||||
[69, ['0074_add_user_home.sql']],
|
||||
[70, ['0075_event-subscriptions.sql']],
|
||||
[71, ['0076_event-handlers.sql']],
|
||||
[72, ['0077_teams.sql']],
|
||||
];
|
||||
|
||||
export class SqliteDatabaseClient extends AbstractDatabaseClient {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
-- Copyright (C) 2024-present Puter Technologies Inc.
|
||||
--
|
||||
-- This file is part of Puter.
|
||||
--
|
||||
-- Puter is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU Affero General Public License as published
|
||||
-- by the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
--
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU Affero General Public License for more details.
|
||||
--
|
||||
-- You should have received a copy of the GNU Affero General Public License
|
||||
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
-- Teams. See sqlite/0077_teams.sql for the column rationale.
|
||||
-- No per-file applied-state tracking, so every statement must tolerate a re-run:
|
||||
-- columns go through _puter_add_col, indexes through the guarded procedure below.
|
||||
|
||||
CALL _puter_add_col('group', 'kind', '`kind` varchar(20) DEFAULT NULL');
|
||||
CALL _puter_add_col('group', 'name', '`name` varchar(255) DEFAULT NULL');
|
||||
CALL _puter_add_col('group', 'handle', '`handle` varchar(64) DEFAULT NULL');
|
||||
CALL _puter_add_col('group', 'deleted_at', '`deleted_at` timestamp NULL DEFAULT NULL');
|
||||
CALL _puter_add_col('jct_user_group', 'org_owned', '`org_owned` tinyint(1) DEFAULT NULL');
|
||||
CALL _puter_add_col('user', 'requires_password_change', '`requires_password_change` tinyint(1) DEFAULT NULL');
|
||||
|
||||
DROP PROCEDURE IF EXISTS _puter_add_team_indexes;
|
||||
DELIMITER //
|
||||
CREATE PROCEDURE _puter_add_team_indexes()
|
||||
BEGIN
|
||||
-- Already case-insensitive: the table is utf8mb4_unicode_ci. NULLs stay distinct.
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'group'
|
||||
AND INDEX_NAME = 'idx_group_handle'
|
||||
) THEN
|
||||
ALTER TABLE `group` ADD UNIQUE INDEX `idx_group_handle` (`handle`);
|
||||
END IF;
|
||||
|
||||
-- Composite, unlike the existing single-column `group_id` key.
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'jct_user_group'
|
||||
AND INDEX_NAME = 'idx_jct_user_group_group'
|
||||
) THEN
|
||||
ALTER TABLE `jct_user_group`
|
||||
ADD INDEX `idx_jct_user_group_group` (`group_id`, `user_id`);
|
||||
END IF;
|
||||
END//
|
||||
DELIMITER ;
|
||||
|
||||
CALL _puter_add_team_indexes();
|
||||
|
||||
DROP PROCEDURE IF EXISTS _puter_add_team_indexes;
|
||||
@@ -0,0 +1,38 @@
|
||||
-- Copyright (C) 2024-present Puter Technologies Inc.
|
||||
--
|
||||
-- This file is part of Puter.
|
||||
--
|
||||
-- Puter is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU Affero General Public License as published
|
||||
-- by the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
--
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU Affero General Public License for more details.
|
||||
--
|
||||
-- You should have received a copy of the GNU Affero General Public License
|
||||
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
-- Teams. See sqlite/0077_teams.sql for the column rationale.
|
||||
-- Idempotent via IF NOT EXISTS; there is no per-file applied-state tracking.
|
||||
|
||||
ALTER TABLE "group" ADD COLUMN IF NOT EXISTS kind text;
|
||||
ALTER TABLE "group" ADD COLUMN IF NOT EXISTS name text;
|
||||
ALTER TABLE "group" ADD COLUMN IF NOT EXISTS handle text;
|
||||
ALTER TABLE "group" ADD COLUMN IF NOT EXISTS deleted_at timestamp;
|
||||
|
||||
-- On lower(handle): text is case-sensitive here, unlike MySQL's utf8mb4_unicode_ci.
|
||||
-- Handle lookups must therefore compare lower(handle) to hit this index.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_group_handle ON "group" (lower(handle));
|
||||
|
||||
-- No idx_group_owner here: idx_group_owner_user_id already covers it.
|
||||
|
||||
ALTER TABLE jct_user_group ADD COLUMN IF NOT EXISTS org_owned smallint;
|
||||
|
||||
-- Composite, unlike the existing single-column idx_jct_user_group_group_id.
|
||||
CREATE INDEX IF NOT EXISTS idx_jct_user_group_group
|
||||
ON jct_user_group (group_id, user_id);
|
||||
|
||||
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS requires_password_change smallint;
|
||||
@@ -0,0 +1,42 @@
|
||||
-- Copyright (C) 2024-present Puter Technologies Inc.
|
||||
--
|
||||
-- This file is part of Puter.
|
||||
--
|
||||
-- Puter is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU Affero General Public License as published
|
||||
-- by the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
--
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU Affero General Public License for more details.
|
||||
--
|
||||
-- You should have received a copy of the GNU Affero General Public License
|
||||
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
-- Teams: a `group` row with `kind = 'team'` owns accounts and pays for them.
|
||||
-- Seeded system groups keep `kind` NULL, so a team query never returns them.
|
||||
|
||||
ALTER TABLE `group` ADD COLUMN `kind` TEXT DEFAULT NULL;
|
||||
ALTER TABLE `group` ADD COLUMN `name` TEXT DEFAULT NULL;
|
||||
ALTER TABLE `group` ADD COLUMN `handle` TEXT DEFAULT NULL;
|
||||
ALTER TABLE `group` ADD COLUMN `deleted_at` TIMESTAMP DEFAULT NULL;
|
||||
|
||||
-- NOCASE so a handle is unique the way a username is (see 0055). MySQL gets this
|
||||
-- from utf8mb4_unicode_ci; postgres needs a lower() index. NULLs stay distinct.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS `idx_group_handle`
|
||||
ON `group` (`handle` COLLATE NOCASE);
|
||||
|
||||
-- mysql and postgres already index this column; sqlite does not.
|
||||
CREATE INDEX IF NOT EXISTS `idx_group_owner` ON `group` (`owner_user_id`);
|
||||
|
||||
-- 1 = workspace-created, 0 = the workspace owner. Decides who pays, not who may read.
|
||||
ALTER TABLE `jct_user_group` ADD COLUMN `org_owned` INTEGER DEFAULT NULL;
|
||||
|
||||
-- Covers "list this workspace's members"; the unique pair index lands in 0072.
|
||||
CREATE INDEX IF NOT EXISTS `idx_jct_user_group_group`
|
||||
ON `jct_user_group` (`group_id`, `user_id`);
|
||||
|
||||
-- Fourth `requires_*` flag, enforced by assertVerifiedAccount. Set on admin reset.
|
||||
ALTER TABLE `user` ADD COLUMN `requires_password_change` INTEGER DEFAULT NULL;
|
||||
Reference in New Issue
Block a user