Changes
14 changed files (+462/-386)
-
-
@@ -3,11 +3,23 @@ // SPDX-License-Identifier: AGPL-3.0-onlyimport { openDB } from "idb"; import { createRandomBytes, packDate } from "./helpers"; import { CONTEXT } from "./symbols"; import { type Context, type RPCMessage, type YamoriDB } from "./types"; import { service } from "./yamori/service"; import { v1 } from "./migrations/v0001"; import { v2 } from "./migrations/v0002"; import { v3 } from "./migrations/v0003"; import { v4 } from "./migrations/v0004"; import { v5 } from "./migrations/v0005"; import { v6 } from "./migrations/v0006"; import { v7 } from "./migrations/v0007"; import { v8 } from "./migrations/v0008"; import { v9 } from "./migrations/v0009"; import { v10 } from "./migrations/v0010"; import { v11 } from "./migrations/v0011"; import { v12 } from "./migrations/v0012"; import { automaticallyProvide } from "./yamori/worker/v1/paid_leave_provision"; class IDBBackend {
-
@@ -22,396 +34,18 @@ return service(request, this[CONTEXT]);} } const migrations = [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12] as const; export async function idbBackend(): Promise<IDBBackend> { const db = await openDB<YamoriDB>("yamori", 12, { // TODO: マイグレーションを個別のファイルに切り分ける async upgrade(db, oldVersion, _newVersion, transaction, _event) { // v1 if (oldVersion < 1) { const workspaces = db.createObjectStore("workspaces", { keyPath: "id", }); workspaces.createIndex("updatedAt", "updatedAt", { unique: false, }); } // v2 if (oldVersion < 2) { const store = transaction.objectStore("workspaces"); for await (const cursor of store.iterate()) { const deletionKey = new Uint8Array(16); self.crypto.getRandomValues(deletionKey); const updateKey = new Uint8Array(16); self.crypto.getRandomValues(updateKey); const workerAddKey = new Uint8Array(16); self.crypto.getRandomValues(workerAddKey); await cursor.update({ ...cursor.value, capabilities: { ...cursor.value.capabilities, deletionKey, updateKey, workerAddKey, }, }); } } // v3 if (oldVersion < 3) { const workers = db.createObjectStore("workers", { keyPath: "id", }); workers.createIndex("workspaceId", "workspaceId", { unique: false, }); workers.createIndex("updatedAt", "updatedAt", { unique: false, }); } // v4 if (oldVersion < 4) { const store = transaction.objectStore("workspaces"); for await (const cursor of store.iterate()) { const createLeaveDefinitionKey = new Uint8Array(16); self.crypto.getRandomValues(createLeaveDefinitionKey); await cursor.update({ ...cursor.value, capabilities: { ...cursor.value.capabilities, createLeaveDefinitionKey, }, }); } } // v5 if (oldVersion < 5) { // v6 でカバーされるためスキップ } // v6 if (oldVersion < 6) { const store = transaction.objectStore("workspaces"); for await (const cursor of store.iterate()) { const defs = cursor.value.leaveDefinitions; await cursor.update({ ...cursor.value, // 労働基準法 第三十九条 第十項 で定義されている「出勤したものとみなす」休業に // ついてはどんな会社でも必ず必須となるため一律で登録する。 leaveDefinitions: [ { id: defs.find( (def) => def.createdBy === "system" && def.displayName === "育児休業", )?.id || `lv-${self.crypto.randomUUID()}`, displayName: "育児休業", updateKey: createRandomBytes(), createdBy: "system", revisions: [ { id: `lr-${self.crypto.randomUUID()}`, // 法律第七十九号 startAtPackedDate: packDate({ year: 1994, month: 4, day: 1, }), snapshot: { isWorkerDeemedToBeWorked: true, }, }, ], }, { id: defs.find( (def) => def.createdBy === "system" && def.displayName === "介護休業", )?.id || `lv-${self.crypto.randomUUID()}`, displayName: "介護休業", updateKey: createRandomBytes(), createdBy: "system", revisions: [ { id: `lr-${self.crypto.randomUUID()}`, // 法律第百七号 startAtPackedDate: packDate({ year: 1995, month: 10, day: 1, }), snapshot: { isWorkerDeemedToBeWorked: true, }, }, ], }, { id: defs.find( (def) => def.createdBy === "system" && def.displayName === "産前産後休業", )?.id || `lv-${self.crypto.randomUUID()}`, displayName: "産前産後休業", updateKey: createRandomBytes(), createdBy: "system", revisions: [ { id: `lr-${self.crypto.randomUUID()}`, // 最初からある (法律第四十九号) // 三十九条の施行年月日は調べてもわからなかったので早い方 (遅い方は11/1) // ただぶっちゃけ昔のことだからシステム運用的にはどっちでもいい。 // こんな昔のデータを入れるとしたら付与日数なんかも過去の計算式を用意 // する必要があるため、あくまでも記録・教育的な側面のデータ。 startAtPackedDate: packDate({ year: 1947, month: 9, day: 1, }), snapshot: { isWorkerDeemedToBeWorked: true, }, }, ], }, ...defs.filter((def) => def.createdBy !== "system"), ], }); } } // v7 if (oldVersion < 7) { const store = transaction.objectStore("workspaces"); for await (const cursor of store.iterate()) { await cursor.update({ ...cursor.value, leaveDefinitions: cursor.value.leaveDefinitions.map((def) => { return { ...def, deletionKey: def.createdBy === "user" ? createRandomBytes(16) : undefined, }; }), }); } } // v8 if (oldVersion < 8) { const store = transaction.objectStore("workers"); for await (const cursor of store.iterate()) { await cursor.update({ ...cursor.value, writeWorkRecordKey: cursor.value.writeWorkRecordKey || createRandomBytes(16), }); } const workRecords = db.createObjectStore("workRecords", { keyPath: "recordId", }); workRecords.createIndex( "workspaceId/workerId/datePacked", ["workspaceId", "workerId", "datePacked"], { unique: true, }, ); } // v9 if (oldVersion < 9) { const store = transaction.objectStore("workers"); for await (const cursor of store.iterate()) { await cursor.update({ ...cursor.value, providePaidLeaveKey: cursor.value.providePaidLeaveKey || createRandomBytes(16), }); } const paidLeaveProvision = db.createObjectStore("paidLeaveProvision", { keyPath: "id", }); paidLeaveProvision.createIndex( "workspaceId,workerId", ["workspaceId", "workerId"], { unique: false }, ); paidLeaveProvision.createIndex( "workspaceId,workerId,providedAtPacked", ["workspaceId", "workerId", "providedAtPacked"], { unique: true }, ); } // v10 if (oldVersion < 10) { const provisionTables = db.createObjectStore("paidLeaveProvisionTable", { keyPath: "id", }); provisionTables.createIndex("id,workspaceId", ["id", "workspaceId"], { unique: true, }); provisionTables.createIndex("workspaceId", "workspaceId", { multiEntry: true, unique: false, }); for (let i = 0, l = migrations.length; i < l; i++) { const version = i + 1; const migration = migrations[i]; const generateID = () => "pt-" + crypto.randomUUID(); const systemTables = [ // https://laws.e-gov.go.jp/law/322AC0000000049#Mp-Ch_4-At_39 { id: generateID(), displayName: "通常", initialRevision: { firstProvisionAmountDays: 10, subsequentProvisionAmountDays: [11, 12, 14, 16, 18, 20], }, revisions: [], workspaceId: "", }, // https://laws.e-gov.go.jp/law/322M40000100023#Mp-At_24_3 { id: generateID(), displayName: "週所定労働日数4日", initialRevision: { firstProvisionAmountDays: 7, subsequentProvisionAmountDays: [8, 9, 10, 12, 13, 15], }, revisions: [], workspaceId: "", }, { id: generateID(), displayName: "週所定労働日数3日", initialRevision: { firstProvisionAmountDays: 5, subsequentProvisionAmountDays: [6, 6, 8, 9, 10, 11], }, revisions: [], workspaceId: "", }, { id: generateID(), displayName: "週所定労働日数2日", initialRevision: { firstProvisionAmountDays: 3, subsequentProvisionAmountDays: [4, 4, 5, 6, 6, 7], }, revisions: [], workspaceId: "", }, { id: generateID(), displayName: "週所定労働日数1日", initialRevision: { firstProvisionAmountDays: 1, subsequentProvisionAmountDays: [2, 2, 2, 3, 3, 3], }, revisions: [], workspaceId: "", }, ] satisfies YamoriDB["paidLeaveProvisionTable"]["value"][]; for (const table of systemTables) { await provisionTables.put(table); } const workspaces = transaction.objectStore("workspaces"); for await (const workspace of workspaces.iterate()) { for (const table of systemTables) { await provisionTables.put({ ...table, id: generateID(), workspaceId: workspace.value.id, baseId: table.id, }); } } } // v11 if (oldVersion < 11) { const store = transaction.objectStore("workRecords"); for await (const record of store.iterate()) { switch (record.value.record?.type) { case "legal_leave": case "special_leave": store.put({ ...record.value, record: { type: "workspace_defined_leave", leave_id: record.value.record.leave_id, }, }); break; } } } // v12 if (oldVersion < 12) { const store = transaction.objectStore("workspaces"); for await (const ws of store.iterate()) { await store.put({ ...ws.value, leaveDefinitions: ws.value.leaveDefinitions.map((def) => { if (def.createdBy !== "system") { return def; } switch (def.displayName) { case "育児休業": return { ...def, abbrName: "育休", }; case "介護休業": return { ...def, abbrName: "介護", }; case "産前産後休業": return { ...def, abbrName: "産休", }; default: return def; } }), abbreviations: { dayoff: "休日", worked: "出勤", skippedWork: "欠勤", paidLeave: "年休", }, }); if (oldVersion < version && migration) { await migration(db, transaction); } } },
-
-
-
@@ -0,0 +1,11 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import type { IDBPDatabase, IDBPTransaction, StoreNames } from "idb"; import type { YamoriDB } from "../types"; export type Migration = ( db: IDBPDatabase<YamoriDB>, transaction: IDBPTransaction<YamoriDB, StoreNames<YamoriDB>[], "versionchange">, ) => Promise<void>;
-
-
-
@@ -0,0 +1,14 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import type { Migration } from "./types"; export const v1: Migration = async (db) => { const workspaces = db.createObjectStore("workspaces", { keyPath: "id", }); workspaces.createIndex("updatedAt", "updatedAt", { unique: false, }); };
-
-
-
@@ -0,0 +1,29 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import type { Migration } from "./types"; export const v2: Migration = async (_db, transaction) => { const store = transaction.objectStore("workspaces"); for await (const cursor of store.iterate()) { const deletionKey = new Uint8Array(16); self.crypto.getRandomValues(deletionKey); const updateKey = new Uint8Array(16); self.crypto.getRandomValues(updateKey); const workerAddKey = new Uint8Array(16); self.crypto.getRandomValues(workerAddKey); await cursor.update({ ...cursor.value, capabilities: { ...cursor.value.capabilities, deletionKey, updateKey, workerAddKey, }, }); } };
-
-
-
@@ -0,0 +1,18 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import type { Migration } from "./types"; export const v3: Migration = async (db) => { const workers = db.createObjectStore("workers", { keyPath: "id", }); workers.createIndex("workspaceId", "workspaceId", { unique: false, }); workers.createIndex("updatedAt", "updatedAt", { unique: false, }); };
-
-
-
@@ -0,0 +1,21 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import type { Migration } from "./types"; export const v4: Migration = async (_db, transaction) => { const store = transaction.objectStore("workspaces"); for await (const cursor of store.iterate()) { const createLeaveDefinitionKey = new Uint8Array(16); self.crypto.getRandomValues(createLeaveDefinitionKey); await cursor.update({ ...cursor.value, capabilities: { ...cursor.value.capabilities, createLeaveDefinitionKey, }, }); } };
-
-
-
@@ -0,0 +1,8 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import type { Migration } from "./types"; export const v5: Migration = async () => { // v6 でカバーされる内容だったため削除された。 };
-
-
-
@@ -0,0 +1,96 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { createRandomBytes, packDate } from "../helpers"; import type { Migration } from "./types"; export const v6: Migration = async (_db, transaction) => { const store = transaction.objectStore("workspaces"); for await (const cursor of store.iterate()) { const defs = cursor.value.leaveDefinitions; await cursor.update({ ...cursor.value, // 労働基準法 第三十九条 第十項 で定義されている「出勤したものとみなす」休業に // ついてはどんな会社でも必ず必須となるため一律で登録する。 leaveDefinitions: [ { id: defs.find( (def) => def.createdBy === "system" && def.displayName === "育児休業", )?.id || `lv-${self.crypto.randomUUID()}`, displayName: "育児休業", updateKey: createRandomBytes(), createdBy: "system", revisions: [ { id: `lr-${self.crypto.randomUUID()}`, // 法律第七十九号 startAtPackedDate: packDate({ year: 1994, month: 4, day: 1, }), snapshot: { isWorkerDeemedToBeWorked: true, }, }, ], }, { id: defs.find( (def) => def.createdBy === "system" && def.displayName === "介護休業", )?.id || `lv-${self.crypto.randomUUID()}`, displayName: "介護休業", updateKey: createRandomBytes(), createdBy: "system", revisions: [ { id: `lr-${self.crypto.randomUUID()}`, // 法律第百七号 startAtPackedDate: packDate({ year: 1995, month: 10, day: 1, }), snapshot: { isWorkerDeemedToBeWorked: true, }, }, ], }, { id: defs.find( (def) => def.createdBy === "system" && def.displayName === "産前産後休業", )?.id || `lv-${self.crypto.randomUUID()}`, displayName: "産前産後休業", updateKey: createRandomBytes(), createdBy: "system", revisions: [ { id: `lr-${self.crypto.randomUUID()}`, // 最初からある (法律第四十九号) // 三十九条の施行年月日は調べてもわからなかったので早い方 (遅い方は11/1) // ただぶっちゃけ昔のことだからシステム運用的にはどっちでもいい。 // こんな昔のデータを入れるとしたら付与日数なんかも過去の計算式を用意 // する必要があるため、あくまでも記録・教育的な側面のデータ。 startAtPackedDate: packDate({ year: 1947, month: 9, day: 1, }), snapshot: { isWorkerDeemedToBeWorked: true, }, }, ], }, ...defs.filter((def) => def.createdBy !== "system"), ], }); } };
-
-
-
@@ -0,0 +1,22 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { createRandomBytes } from "../helpers"; import type { Migration } from "./types"; export const v7: Migration = async (_db, transaction) => { const store = transaction.objectStore("workspaces"); for await (const cursor of store.iterate()) { await cursor.update({ ...cursor.value, leaveDefinitions: cursor.value.leaveDefinitions.map((def) => { return { ...def, deletionKey: def.createdBy === "user" ? createRandomBytes(16) : undefined, }; }), }); } };
-
-
-
@@ -0,0 +1,29 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { createRandomBytes } from "../helpers"; import type { Migration } from "./types"; export const v8: Migration = async (db, transaction) => { const store = transaction.objectStore("workers"); for await (const cursor of store.iterate()) { await cursor.update({ ...cursor.value, writeWorkRecordKey: cursor.value.writeWorkRecordKey || createRandomBytes(16), }); } const workRecords = db.createObjectStore("workRecords", { keyPath: "recordId", }); workRecords.createIndex( "workspaceId/workerId/datePacked", ["workspaceId", "workerId", "datePacked"], { unique: true, }, ); };
-
-
-
@@ -0,0 +1,31 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { createRandomBytes } from "../helpers"; import type { Migration } from "./types"; export const v9: Migration = async (db, transaction) => { const store = transaction.objectStore("workers"); for await (const cursor of store.iterate()) { await cursor.update({ ...cursor.value, providePaidLeaveKey: cursor.value.providePaidLeaveKey || createRandomBytes(16), }); } const paidLeaveProvision = db.createObjectStore("paidLeaveProvision", { keyPath: "id", }); paidLeaveProvision.createIndex("workspaceId,workerId", ["workspaceId", "workerId"], { unique: false, }); paidLeaveProvision.createIndex( "workspaceId,workerId,providedAtPacked", ["workspaceId", "workerId", "providedAtPacked"], { unique: true }, ); };
-
-
-
@@ -0,0 +1,95 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import type { YamoriDB } from "../types"; import type { Migration } from "./types"; export const v10: Migration = async (db, transaction) => { const provisionTables = db.createObjectStore("paidLeaveProvisionTable", { keyPath: "id", }); provisionTables.createIndex("id,workspaceId", ["id", "workspaceId"], { unique: true, }); provisionTables.createIndex("workspaceId", "workspaceId", { multiEntry: true, unique: false, }); const generateID = () => "pt-" + crypto.randomUUID(); const systemTables = [ // https://laws.e-gov.go.jp/law/322AC0000000049#Mp-Ch_4-At_39 { id: generateID(), displayName: "通常", initialRevision: { firstProvisionAmountDays: 10, subsequentProvisionAmountDays: [11, 12, 14, 16, 18, 20], }, revisions: [], workspaceId: "", }, // https://laws.e-gov.go.jp/law/322M40000100023#Mp-At_24_3 { id: generateID(), displayName: "週所定労働日数4日", initialRevision: { firstProvisionAmountDays: 7, subsequentProvisionAmountDays: [8, 9, 10, 12, 13, 15], }, revisions: [], workspaceId: "", }, { id: generateID(), displayName: "週所定労働日数3日", initialRevision: { firstProvisionAmountDays: 5, subsequentProvisionAmountDays: [6, 6, 8, 9, 10, 11], }, revisions: [], workspaceId: "", }, { id: generateID(), displayName: "週所定労働日数2日", initialRevision: { firstProvisionAmountDays: 3, subsequentProvisionAmountDays: [4, 4, 5, 6, 6, 7], }, revisions: [], workspaceId: "", }, { id: generateID(), displayName: "週所定労働日数1日", initialRevision: { firstProvisionAmountDays: 1, subsequentProvisionAmountDays: [2, 2, 2, 3, 3, 3], }, revisions: [], workspaceId: "", }, ] satisfies YamoriDB["paidLeaveProvisionTable"]["value"][]; for (const table of systemTables) { await provisionTables.put(table); } const workspaces = transaction.objectStore("workspaces"); for await (const workspace of workspaces.iterate()) { for (const table of systemTables) { await provisionTables.put({ ...table, id: generateID(), workspaceId: workspace.value.id, baseId: table.id, }); } } };
-
-
-
@@ -0,0 +1,23 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import type { Migration } from "./types"; export const v11: Migration = async (_db, transaction) => { const store = transaction.objectStore("workRecords"); for await (const record of store.iterate()) { switch (record.value.record?.type) { case "legal_leave": case "special_leave": store.put({ ...record.value, record: { type: "workspace_defined_leave", leave_id: record.value.record.leave_id, }, }); break; } } };
-
-
-
@@ -0,0 +1,45 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import type { Migration } from "./types"; export const v12: Migration = async (_db, transaction) => { const store = transaction.objectStore("workspaces"); for await (const ws of store.iterate()) { await store.put({ ...ws.value, leaveDefinitions: ws.value.leaveDefinitions.map((def) => { if (def.createdBy !== "system") { return def; } switch (def.displayName) { case "育児休業": return { ...def, abbrName: "育休", }; case "介護休業": return { ...def, abbrName: "介護", }; case "産前産後休業": return { ...def, abbrName: "産休", }; default: return def; } }), abbreviations: { dayoff: "休日", worked: "出勤", skippedWork: "欠勤", paidLeave: "年休", }, }); } };
-