feat: Initial clean release of CHM
一些检查失败了
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

这个提交包含在:
chen
2026-11-04 11:29:39 +08:00
当前提交 301194963b
共有 8663 个文件被更改,包括 1962691 次插入 和 0 次删除

查看文件

@@ -0,0 +1,66 @@
import { ModelType } from '../../../BaseModel';
import { ModelFeedPage } from '../../../models/utils/paginatedFeed';
import { PaginationOrderDir } from '../../../models/utils/types';
import { Request } from '../Api';
import requestPaginationOptions from '../utils/requestPaginationOptions';
import requestFields from './requestFields';
export interface Options {
sort?: boolean;
}
// Note that this is inefficient pagination as it requires all the items to
// have been loaded first, even if not all of them are returned.
//
// It's however convenient for smaller lists as it reduces the need for
// building complex SQL queries.
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Callers pass entity types (NoteEntity, FolderEntity, etc.) which lack index signatures
export default function(itemType: ModelType, items: any[], request: Request, options: Options = null): ModelFeedPage {
options = {
sort: true,
...options,
};
const fields = requestFields(request, itemType);
const pagination = requestPaginationOptions(request);
const startIndex = (pagination.page - 1) * pagination.limit;
const itemCount = Math.min(items.length - startIndex, pagination.limit);
const hasMore = itemCount >= pagination.limit;
const sortBy = pagination.order[0].by;
const sortDir = pagination.order[0].dir;
const caseInsensitive = pagination.order[0].caseInsensitive;
const sortedItems = items.slice().map(item => {
if (!fields.length) return item;
const newItem: Record<string, unknown> = {};
for (const k of Object.keys(item)) {
if (!fields.includes(k)) continue;
newItem[k] = item[k];
}
return newItem;
});
if (options.sort) {
sortedItems.sort((a, b) => {
let v1: unknown = a && (sortBy in a) ? a[sortBy] : '';
let v2: unknown = b && (sortBy in b) ? b[sortBy] : '';
if (caseInsensitive && typeof v1 === 'string') v1 = v1.toLowerCase();
if (caseInsensitive && typeof v2 === 'string') v2 = v2.toLowerCase();
if (sortDir === PaginationOrderDir.ASC) {
if (v1 < v2) return -1;
if (v2 > v1) return +1;
} else {
if (v1 > v2) return -1;
if (v2 < v1) return +1;
}
return 0;
});
}
return {
items: sortedItems.slice(startIndex, startIndex + itemCount),
has_more: hasMore,
};
}

查看文件

@@ -0,0 +1,53 @@
import { Request, RequestMethod } from '../Api';
import defaultSaveOptions from './defaultSaveOptions';
import { ErrorMethodNotAllowed, ErrorNotFound } from './errors';
import paginatedResults from './paginatedResults';
import readonlyProperties from './readonlyProperties';
import requestFields from './requestFields';
import BaseItem from '../../../models/BaseItem';
import { LoadOptions } from '../../../models/utils/types';
import { WhereQuery } from '../../../models/utils/paginatedFeed';
export default async function(modelType: number, request: Request, id: string = null, link: string = null, defaultFields: string[] = null, whereQuery: WhereQuery = null) {
if (link) throw new ErrorNotFound(); // Default action doesn't support links at all for now
const ModelClass = BaseItem.getClassByItemType(modelType);
const getOneModel = async (options: LoadOptions = null) => {
const model = await ModelClass.load(id, options || {});
if (!model) throw new ErrorNotFound();
return model;
};
if (request.method === 'GET') {
if (id) {
return getOneModel({
fields: requestFields(request, modelType, defaultFields),
});
} else {
return paginatedResults(modelType, request, whereQuery, defaultFields);
}
}
if (request.method === 'PUT' && id) {
const model = await getOneModel();
const newModel = { ...model, ...request.bodyJson(readonlyProperties('PUT')) };
return ModelClass.save(newModel, { userSideValidation: true });
}
if (request.method === 'DELETE' && id) {
const model = await getOneModel();
await ModelClass.delete(model.id, { source: 'API: DELETE method' });
return;
}
if (request.method === RequestMethod.POST) {
const props = readonlyProperties('POST');
const idIdx = props.indexOf('id');
if (idIdx >= 0) props.splice(idIdx, 1);
const model = request.bodyJson(props);
return ModelClass.save(model, defaultSaveOptions('POST', model.id));
}
throw new ErrorMethodNotAllowed();
}

查看文件

@@ -0,0 +1,10 @@
import { Request } from '../Api';
import { LoadOptions } from '../../../models/utils/types';
import requestFields from './requestFields';
export default function(request: Request, modelType: number): LoadOptions {
const options: LoadOptions = {};
const fields = requestFields(request, modelType);
if (fields.length) options.fields = fields;
return options;
}

查看文件

@@ -0,0 +1,11 @@
export interface DefaultSaveOptions {
userSideValidation: boolean;
isNew?: boolean;
autoTimestamp?: boolean;
}
export default function(requestMethod: string, modelId: string = null): DefaultSaveOptions {
const options: DefaultSaveOptions = { userSideValidation: true };
if (requestMethod === 'POST' && modelId) options.isNew = true;
return options;
}

查看文件

@@ -0,0 +1,33 @@
class ApiError extends Error {
private httpCode_: number;
public constructor(message: string, httpCode = 400) {
super(message);
this.httpCode_ = httpCode;
}
public get httpCode() {
return this.httpCode_;
}
}
export class ErrorMethodNotAllowed extends ApiError {
public constructor(message = 'Method Not Allowed') {
super(message, 405);
}
}
export class ErrorNotFound extends ApiError {
public constructor(message = 'Not Found') {
super(message, 404);
}
}
export class ErrorForbidden extends ApiError {
public constructor(message = 'Forbidden') {
super(message, 403);
}
}
export class ErrorBadRequest extends ApiError {
public constructor(message = 'Bad Request') {
super(message, 400);
}
}

查看文件

@@ -0,0 +1,13 @@
import { Request } from '../Api';
import requestFields from './requestFields';
import BaseModel from '../../../BaseModel';
import requestPaginationOptions from './requestPaginationOptions';
import paginatedFeed, { WhereQuery, ModelFeedPage } from '../../../models/utils/paginatedFeed';
import BaseItem from '../../../models/BaseItem';
export default async function(modelType: number, request: Request, whereQuery: WhereQuery = null, defaultFields: string[] = null): Promise<ModelFeedPage> {
const ModelClass = BaseItem.getClassByItemType(modelType);
const fields = requestFields(request, modelType, defaultFields);
const pagination = requestPaginationOptions(request);
return paginatedFeed(BaseModel.db(), ModelClass.tableName(), pagination, whereQuery, fields);
}

查看文件

@@ -0,0 +1,5 @@
export default function readonlyProperties(requestMethod: string) {
const output = ['created_time', 'updated_time', 'encryption_blob_encrypted', 'encryption_applied', 'encryption_cipher_text'];
if (requestMethod !== 'POST') output.splice(0, 0, 'id');
return output;
}

查看文件

@@ -0,0 +1,28 @@
import { Request } from '../Api';
import BaseItem from '../../../models/BaseItem';
function defaultFieldsByModelType(modelType: number): string[] {
const ModelClass = BaseItem.getClassByItemType(modelType);
const possibleFields = ['id', 'parent_id', 'title', 'deleted_time'];
const output = [];
for (const f of possibleFields) {
if (ModelClass.hasField(f)) output.push(f);
}
return output;
}
export default function(request: Request, modelType: number, defaultFields: string[] = null) {
const getDefaults = () => {
if (defaultFields) return defaultFields;
return defaultFieldsByModelType(modelType);
};
const query = request.query;
if (!query || !query.fields) return getDefaults();
if (Array.isArray(query.fields)) return query.fields.slice();
const fields = query.fields
.split(',')
.map((f: string) => f.trim())
.filter((f: string) => !!f);
return fields.length ? fields : getDefaults();
}

查看文件

@@ -0,0 +1,23 @@
import { Pagination, PaginationOrder, PaginationOrderDir } from '../../../models/utils/types';
import { Request } from '../Api';
import { ErrorBadRequest } from './errors';
function requestPaginationOrder(request: Request): PaginationOrder[] {
const orderBy: string = request.query.order_by ? request.query.order_by : 'updated_time';
const orderDir: PaginationOrderDir = request.query.order_dir ? request.query.order_dir : PaginationOrderDir.ASC;
return [{
by: orderBy,
dir: orderDir,
caseInsensitive: true,
}];
}
export default function(request: Request): Pagination {
const limit = request.query.limit ? request.query.limit : 100;
if (limit < 0 || limit > 100) throw new ErrorBadRequest(`Limit out of bond: ${limit}`);
const order: PaginationOrder[] = requestPaginationOrder(request);
const page: number = request.query.page || 1;
return { limit, order, page };
}