Changes
9 changed files (+372/-24)
-
-
@@ -6,7 +6,7 @@import { create } from "@bufbuild/protobuf"; import { DateSchema } from "@yamori/proto/yamori/type/v1/date_pb.js"; import { isSameBytes, maskMessage } from "./helpers"; import { isSameBytes, maskMessage, packDate, unpackDate } from "./helpers"; describe("isSameBytes", () => { test("Should return true for same references", () => {
-
@@ -123,3 +123,69 @@ expect(input.month).toBe(1);expect(input.day).toBe(1); }); }); describe("packDate / unpackDate", () => { test("Should pack and unpack", () => { expect( unpackDate( packDate({ year: 2021, month: 9, day: 31, }), ), ).toEqual({ year: 2021, month: 9, day: 31, }); }); test("Should support far future years", () => { expect( unpackDate( packDate({ year: 3600, month: 2, day: 29, }), ), ).toEqual({ year: 3600, month: 2, day: 29, }); }); test("Should produce sortable numbers", () => { const input = [ { year: 2000, month: 3, day: 1, }, { year: 2000, month: 1, day: 1, }, { year: 2999, month: 1, day: 1, }, { year: 2000, month: 1, day: 2, }, ] as const; expect( input .map(packDate) .sort((a, b) => a - b) .map(unpackDate), ).toEqual([input[1], input[3], input[0], input[2]]); }); });
-
-
-
@@ -48,3 +48,41 @@ }return masked; } /** * ECMAScript の Date とごっちゃにならないように名前を変えている。 */ export interface CalendarDate { /** * 1~ */ year: number; /** * 1~12 */ month: number; /** * 1~31 */ day: number; } export function packDate({ year, month, day }: CalendarDate): number { return (year << 9) | (Math.min(month, 12) << 5) | (Math.min(day, 31) & 0b11111); } export function unpackDate(date: number): CalendarDate { return { year: date >> 9, month: (date >> 5) & 0b1111, day: date & 0b11111, }; } export function createRandomBytes(byteLength: number = 16): Uint8Array { const buffer = new Uint8Array(byteLength); self.crypto.getRandomValues(buffer); return buffer; }
-
-
-
@@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-onlyimport { openDB } from "idb"; import { createRandomBytes, packDate } from "./helpers"; import { CONTEXT } from "./symbols"; import { type Context, type RPCMessage } from "./types"; import { service } from "./yamori/service";
-
@@ -21,7 +22,7 @@ }export async function idbBackend(): Promise<IDBBackend> { return new IDBBackend({ db: await openDB("yamori", 5, { db: await openDB("yamori", 6, { async upgrade(db, oldVersion, _newVersion, transaction, _event) { // v1 if (oldVersion < 1) {
-
@@ -95,35 +96,98 @@ }// 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: `lv-${self.crypto.randomUUID()}`, id: defs.find( (def) => def.createdBy === "system" && def.displayName === "育児休業", )?.id || `lv-${self.crypto.randomUUID()}`, displayName: "育児休業", isWorkerDeemedToBeWorked: true, updateKey: null, updateKey: createRandomBytes(), createdBy: "system", revisions: [ { id: `lr-${self.crypto.randomUUID()}`, // 法律第七十九号 startAtPackedDate: packDate({ year: 1994, month: 4, day: 1, }), snapshot: { isWorkerDeemedToBeWorked: true, }, }, ], }, { id: `lv-${self.crypto.randomUUID()}`, id: defs.find( (def) => def.createdBy === "system" && def.displayName === "介護休業", )?.id || `lv-${self.crypto.randomUUID()}`, displayName: "介護休業", isWorkerDeemedToBeWorked: true, updateKey: null, updateKey: createRandomBytes(), createdBy: "system", revisions: [ { id: `lr-${self.crypto.randomUUID()}`, // 法律第百七号 startAtPackedDate: packDate({ year: 1995, month: 10, day: 1, }), snapshot: { isWorkerDeemedToBeWorked: true, }, }, ], }, { id: `lv-${self.crypto.randomUUID()}`, id: defs.find( (def) => def.createdBy === "system" && def.displayName === "産前産後休業", )?.id || `lv-${self.crypto.randomUUID()}`, displayName: "産前産後休業", isWorkerDeemedToBeWorked: true, updateKey: null, 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"), ], }); }
-
-
-
@@ -19,10 +19,16 @@ createLeaveDefinitionKey: Uint8Array;}; leaveDefinitions: { id: string; isWorkerDeemedToBeWorked: boolean; displayName: string; updateKey: Uint8Array | null; createdBy: "user" | "system"; revisions: { id: string; startAtPackedDate: number; snapshot: { isWorkerDeemedToBeWorked: boolean; }; }[]; }[]; updatedAt: number; };
-
-
-
@@ -3,19 +3,58 @@ // SPDX-License-Identifier: AGPL-3.0-onlyimport { create, type MessageShape, type MessageInitShape } from "@bufbuild/protobuf"; import { LeaveSchema } from "@yamori/proto/yamori/work_record/v1/leave_pb.js"; import { type LeaveRevisionSchema } from "@yamori/proto/yamori/work_record/v1/leave_revision_pb.js"; import { type LeaveReadMask } from "@yamori/proto/yamori/work_record/v1/leave_read_mask_pb.js"; import { maskMessage } from "../../../helpers"; import { maskMessage, unpackDate } from "../../../helpers"; import { type YamoriDB } from "../../../types"; export function fromDBEntry( entry: YamoriDB["workspaces"]["value"]["leaveDefinitions"][number], ): MessageShape<typeof LeaveSchema> { const revisions = entry.revisions.map<MessageInitShape<typeof LeaveRevisionSchema>>( (raw) => { return { revisionId: { value: raw.id }, startAt: unpackDate(raw.startAtPackedDate), snapshot: { isWorkerDeemedToBeWorked: raw.snapshot.isWorkerDeemedToBeWorked, }, }; }, ); // 比較対象の両方の日付を現地時間に変換するためタイムゾーンを気にする必要はない。 // TODO: DST に対応できるか検証 const now = new Date(); let currentRevision: MessageInitShape<typeof LeaveRevisionSchema> | null = null; for (const rev of revisions) { if ( !rev.startAt || typeof rev.startAt.year !== "number" || typeof rev.startAt.month !== "number" || typeof rev.startAt.day !== "number" ) { continue; } const startAt = new Date(rev.startAt.year, rev.startAt.month - 1, rev.startAt.day); if (now >= startAt) { currentRevision = rev; } currentRevision = rev; } return create(LeaveSchema, { id: { value: entry.id }, isWorkerDeemedToBeWorked: entry.isWorkerDeemedToBeWorked, isWorkerDeemedToBeWorked: currentRevision?.snapshot?.isWorkerDeemedToBeWorked ?? false, displayName: entry.displayName, updateKey: entry.updateKey ? { key: entry.updateKey } : undefined, currentRevision: currentRevision ?? undefined, revisions, }); }
-
-
-
@@ -230,6 +230,15 @@ expect.objectContaining({id: expect.anything(), displayName: "育児休業", isWorkerDeemedToBeWorked: true, revisions: [ expect.objectContaining({ revisionId: expect.anything(), startAt: expect.anything(), snapshot: expect.objectContaining({ isWorkerDeemedToBeWorked: true, }), }), ], }), );
-
@@ -238,6 +247,15 @@ expect.objectContaining({id: expect.anything(), displayName: "介護休業", isWorkerDeemedToBeWorked: true, revisions: [ expect.objectContaining({ revisionId: expect.anything(), startAt: expect.anything(), snapshot: expect.objectContaining({ isWorkerDeemedToBeWorked: true, }), }), ], }), );
-
@@ -246,6 +264,15 @@ expect.objectContaining({id: expect.anything(), displayName: "産前産後休業", isWorkerDeemedToBeWorked: true, revisions: [ expect.objectContaining({ revisionId: expect.anything(), startAt: expect.anything(), snapshot: expect.objectContaining({ isWorkerDeemedToBeWorked: true, }), }), ], }), ); });
-
-
-
@@ -9,6 +9,7 @@ } from "@yamori/proto/yamori/workspace/v1/create_request_pb.js";import { CreateResponseSchema } from "@yamori/proto/yamori/workspace/v1/create_response_pb.js"; import { type Workspace } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js"; import { createRandomBytes, packDate } from "../../../../helpers"; import { type Context } from "../../../../types"; import * as workspace from "../workspace";
-
@@ -83,23 +84,66 @@ leaveDefinitions: [{ id: `lv-${self.crypto.randomUUID()}`, displayName: "育児休業", isWorkerDeemedToBeWorked: true, updateKey: null, updateKey: createRandomBytes(16), createdBy: "system", revisions: [ { id: `lr-${self.crypto.randomUUID()}`, // 法律第七十九号 startAtPackedDate: packDate({ year: 1994, month: 4, day: 1, }), snapshot: { isWorkerDeemedToBeWorked: true, }, }, ], }, { id: `lv-${self.crypto.randomUUID()}`, displayName: "介護休業", isWorkerDeemedToBeWorked: true, updateKey: null, updateKey: createRandomBytes(16), createdBy: "system", revisions: [ { id: `lr-${self.crypto.randomUUID()}`, // 法律第百七号 startAtPackedDate: packDate({ year: 1995, month: 10, day: 1, }), snapshot: { isWorkerDeemedToBeWorked: true, }, }, ], }, { id: `lv-${self.crypto.randomUUID()}`, displayName: "産前産後休業", isWorkerDeemedToBeWorked: true, updateKey: null, updateKey: createRandomBytes(16), createdBy: "system", revisions: [ { id: `lr-${self.crypto.randomUUID()}`, // 最初からある (法律第四十九号) // 三十九条の施行年月日は調べてもわからなかったので早い方 (遅い方は11/1) // ただぶっちゃけ昔のことだからシステム運用的にはどっちでもいい。 // こんな昔のデータを入れるとしたら付与日数なんかも過去の計算式を用意 // する必要があるため、あくまでも記録・教育的な側面のデータ。 startAtPackedDate: packDate({ year: 1947, month: 9, day: 1, }), snapshot: { isWorkerDeemedToBeWorked: true, }, }, ], }, ], updatedAt: Date.now(),
-
-
-
@@ -131,6 +131,12 @@const resp = await createLeaveDefinition(ctx, { leaveDefinition: { displayName: "Bar", revisions: [ { startAt: { year: 2000, month: 1, day: 1 }, snapshot: { isWorkerDeemedToBeWorked: false }, }, ], }, createLeaveDefinitionKey: created.createLeaveDefinitionKey, });
-
@@ -174,6 +180,12 @@ const resp = await createLeaveDefinition(ctx, {workspaceId: created.id, leaveDefinition: { displayName: "", revisions: [ { startAt: { year: 2000, month: 1, day: 1 }, snapshot: { isWorkerDeemedToBeWorked: false }, }, ], }, createLeaveDefinitionKey: created.createLeaveDefinitionKey, });
-
@@ -196,6 +208,12 @@ const resp = await createLeaveDefinition(ctx, {workspaceId: created.id, leaveDefinition: { displayName: "Bar", revisions: [ { startAt: { year: 2000, month: 1, day: 1 }, snapshot: { isWorkerDeemedToBeWorked: false }, }, ], }, createLeaveDefinitionKey: { key: new Uint8Array([0, 0, 0]) }, });
-
@@ -220,6 +238,12 @@ value: `${created.id?.value}-FOO`,}, leaveDefinition: { displayName: "Bar", revisions: [ { startAt: { year: 2000, month: 1, day: 1 }, snapshot: { isWorkerDeemedToBeWorked: false }, }, ], }, createLeaveDefinitionKey: created.createLeaveDefinitionKey, });
-
@@ -239,8 +263,13 @@const resp = await createLeaveDefinition(ctx, { workspaceId: created.id, leaveDefinition: { isWorkerDeemedToBeWorked: true, displayName: "Bar", revisions: [ { startAt: { year: 2000, month: 1, day: 1 }, snapshot: { isWorkerDeemedToBeWorked: true }, }, ], }, createLeaveDefinitionKey: created.createLeaveDefinitionKey, });
-
@@ -267,5 +296,11 @@ }expect(def.updateKey).toHaveProperty("key"); expect(def.displayName).toBe("Bar"); expect(def.isWorkerDeemedToBeWorked).toBe(true); expect(def.currentRevision?.snapshot?.isWorkerDeemedToBeWorked).toBe(true); expect(def.currentRevision?.startAt).toMatchObject({ year: 2000, month: 1, day: 1, }); expect(def.revisions).toHaveLength(1); });
-
-
-
@@ -8,7 +8,7 @@ type CreateLeaveDefinitionRequest,} from "@yamori/proto/yamori/workspace/v1/create_leave_definition_request_pb.js"; import { CreateLeaveDefinitionResponseSchema } from "@yamori/proto/yamori/workspace/v1/create_leave_definition_response_pb.js"; import { isSameBytes } from "../../../../helpers"; import { isSameBytes, packDate } from "../../../../helpers"; import { type Context, type YamoriDB } from "../../../../types"; import * as leave from "../../../work_record/v1/leave";
-
@@ -84,6 +84,27 @@ },}); } if (req.leaveDefinition.revisions.length === 0) { return respond({ case: "missingField", value: { path: "leave_definition.revisions" }, }); } if (req.leaveDefinition.revisions.some((revision) => !revision.startAt)) { return respond({ case: "missingField", value: { path: "leave_definition.revisions.start_at" }, }); } if (req.leaveDefinition.revisions.some((revision) => !revision.snapshot)) { return respond({ case: "missingField", value: { path: "leave_definition.revisions.snapshot" }, }); } try { const tx = db.transaction("workspaces", "readwrite"); const store = tx.objectStore("workspaces");
-
@@ -118,10 +139,18 @@ self.crypto.getRandomValues(updateKey);const entry: YamoriDB["workspaces"]["value"]["leaveDefinitions"][number] = { id: `lv-${self.crypto.randomUUID()}`, isWorkerDeemedToBeWorked: req.leaveDefinition.isWorkerDeemedToBeWorked, displayName: req.leaveDefinition.displayName, updateKey, createdBy: "user", revisions: req.leaveDefinition.revisions.map((revision) => { return { id: `lr-${self.crypto.randomUUID()}`, startAtPackedDate: packDate(revision.startAt!), snapshot: { isWorkerDeemedToBeWorked: revision.snapshot!.isWorkerDeemedToBeWorked, }, }; }), }; await store.put({
-