文件
mynode/packages/tools/fuzzer/model/FolderRecord.ts
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

218 行
5.3 KiB
TypeScript

import { strict as assert } from 'node:assert';
import type { FolderData, ItemId } from './types';
import Serializable, { BaseSchema } from './Serializable';
export type ShareRecord = {
email: string;
readOnly: boolean;
};
interface InitializationOptions extends FolderData {
childIds: readonly ItemId[];
sharedWith: readonly ShareRecord[];
isShared: boolean;
// Email of the Joplin Server account that controls the item
ownedByEmail: string;
}
const validateId = (id: string) => {
return !!id.match(/^[a-zA-Z0-9]{32}$/);
};
const schema = {
isFolder: 'boolean',
parentId: 'id',
id: 'id',
title: 'string',
ownedByEmail: 'string',
childIds: 'id[]',
isShared: 'boolean',
sharedWith: [
{ email: 'string', readOnly: 'boolean' },
'...',
],
} satisfies BaseSchema;
const normalizeFolderTitle = (title: string) => {
// Joplin removes / and \ from the beginning of folder titles.
// For consistency, this must be done in the fuzzer, too.
const illegalStartCharRegex = /^[/\\]+/;
return title.replace(illegalStartCharRegex, '');
};
export default class FolderRecord extends Serializable<typeof schema> implements FolderData {
public readonly parentId: ItemId;
public readonly id: ItemId;
public readonly title: string;
public readonly ownedByEmail: string;
public readonly childIds: readonly ItemId[];
private readonly isShared_: boolean;
private readonly sharedWith_: readonly ShareRecord[];
public constructor(options: InitializationOptions) {
super(schema);
this.parentId = options.parentId;
this.id = options.id;
this.title = normalizeFolderTitle(options.title);
this.childIds = options.childIds;
this.isShared_ = options.isShared ?? options.sharedWith.length > 0;
this.ownedByEmail = options.ownedByEmail;
this.sharedWith_ = options.sharedWith;
if (this.parentId !== '' && !validateId(this.parentId)) {
throw new Error(`Invalid parent ID: ${this.parentId}`);
}
if (!validateId(this.id)) {
throw new Error(`Invalid ID: ${this.id}`);
}
}
public static fromSerialized(serialized: unknown) {
const data = this.deserialize(schema, serialized);
return new FolderRecord(data);
}
public serialize() {
return {
isFolder: true,
parentId: this.parentId,
id: this.id,
title: this.title,
ownedByEmail: this.ownedByEmail,
childIds: [...this.childIds],
sharedWith: [...this.sharedWith_],
isShared: this.isShared_,
};
}
public get shareRecipients() {
return this.sharedWith_.map(sharee => sharee.email);
}
private get metadata_(): InitializationOptions {
return {
parentId: this.parentId,
id: this.id,
title: this.title,
isShared: this.isShared_,
ownedByEmail: this.ownedByEmail,
childIds: [...this.childIds],
sharedWith: [...this.sharedWith_],
};
}
public get isRootSharedItem() {
return this.isShared_ && this.parentId === '';
}
public isSharedWith(email: string) {
assert.equal(this.parentId, '', 'only supported for toplevel folders');
return this.sharedWith_.some(record => record.email === email);
}
public isReadOnlySharedWith(email: string) {
assert.equal(this.parentId, '', 'only supported for toplevel folders');
return this.sharedWith_.some(record => record.email === email && record.readOnly);
}
public withTitle(title: string) {
return new FolderRecord({
...this.metadata_,
title,
});
}
public withParent(parentId: ItemId) {
return new FolderRecord({
...this.metadata_,
parentId,
});
}
public withId(id: ItemId) {
return new FolderRecord({
...this.metadata_,
id,
});
}
public withChildren(childIds: readonly ItemId[]) {
return new FolderRecord({
...this.metadata_,
childIds: [...childIds],
});
}
public withChildAdded(childId: ItemId) {
if (this.childIds.includes(childId)) {
return this;
}
return this.withChildren([...this.childIds, childId]);
}
public withChildRemoved(childId: ItemId) {
return this.withChildren(
this.childIds.filter(id => id !== childId),
);
}
private withShareState_(isShared: boolean, shareState: readonly ShareRecord[]) {
return new FolderRecord({
...this.metadata_,
isShared,
sharedWith: [...shareState],
});
}
public withShareOwner(email: string) {
if (email === this.ownedByEmail) return this;
const sharedWith = this.metadata_.sharedWith.filter(recipient => recipient.email !== email);
return new FolderRecord({
...this.metadata_,
sharedWith: sharedWith,
isShared: sharedWith.length > 0,
ownedByEmail: email,
});
}
public withShared(recipientEmail: string, readOnly: boolean) {
if (this.isSharedWith(recipientEmail) && this.isReadOnlySharedWith(recipientEmail) === readOnly) {
return this;
}
if (this.parentId !== '') {
throw new Error('Cannot share non-top-level folder');
}
return this.withShareState_(true, [
...this.sharedWith_.filter(record => record.email !== recipientEmail),
{ email: recipientEmail, readOnly },
]);
}
public withRemovedFromShare(recipientEmail: string) {
if (!this.isSharedWith(recipientEmail)) {
return this;
}
return this.withShareState_(
// Even if no users are present in the share, Joplin still considers the folder to be shared
this.isShared_,
this.sharedWith_.filter(record => record.email !== recipientEmail),
);
}
public withUnshared() {
if (!this.isShared_) {
return this;
}
return this.withShareState_(false, []);
}
}