1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00
joplin/packages/server/src/db.ts
Laurent 0765cf5955
All: Add support for sharing notebooks with Joplin Server (#4772)
- Joplin Server: Adds support for sharing a notebook
- Desktop: Adds support for sharing a notebook with Joplin Server
- Mobile: Adds support for reading and writing to a shared notebook (not possible to share a notebook)
- Cli: Adds support for reading and writing to a shared notebook (not possible to share a notebook)
2021-05-13 17:57:37 +01:00

487 lines
11 KiB
TypeScript

import { knex, Knex } from 'knex';
import { DatabaseConfig } from './utils/types';
import * as pathUtils from 'path';
import time from '@joplin/lib/time';
import Logger from '@joplin/lib/Logger';
// Make sure bigInteger values are numbers and not strings
//
// https://github.com/brianc/node-pg-types
//
// In our case, all bigInteger are timestamps, which JavaScript can handle
// fine as numbers.
require('pg').types.setTypeParser(20, function(val: any) {
return parseInt(val, 10);
});
const logger = Logger.create('db');
const migrationDir = `${__dirname}/migrations`;
const sqliteDbDir = pathUtils.dirname(__dirname);
export const defaultAdminEmail = 'admin@localhost';
export const defaultAdminPassword = 'admin';
export type DbConnection = Knex;
export interface DbConfigConnection {
host?: string;
port?: number;
user?: string;
database?: string;
filename?: string;
password?: string;
}
export interface KnexDatabaseConfig {
client: string;
connection: DbConfigConnection;
useNullAsDefault?: boolean;
asyncStackTraces?: boolean;
}
export interface ConnectionCheckResult {
isCreated: boolean;
error: any;
latestMigration: any;
connection: DbConnection;
}
export function sqliteFilePath(name: string): string {
return `${sqliteDbDir}/db-${name}.sqlite`;
}
export function makeKnexConfig(dbConfig: DatabaseConfig): KnexDatabaseConfig {
const connection: DbConfigConnection = {};
if (dbConfig.client === 'sqlite3') {
connection.filename = sqliteFilePath(dbConfig.name);
} else {
connection.database = dbConfig.name;
connection.host = dbConfig.host;
connection.port = dbConfig.port;
connection.user = dbConfig.user;
connection.password = dbConfig.password;
}
return {
client: dbConfig.client,
useNullAsDefault: dbConfig.client === 'sqlite3',
asyncStackTraces: dbConfig.asyncStackTraces,
connection,
};
}
export async function waitForConnection(dbConfig: DatabaseConfig): Promise<ConnectionCheckResult> {
const timeout = 30000;
const startTime = Date.now();
let lastError = { message: '' };
while (true) {
try {
const connection = await connectDb(dbConfig);
const check = await connectionCheck(connection);
if (check.error) throw check.error;
return check;
} catch (error) {
logger.info('Could not connect. Will try again.', error.message);
lastError = error;
}
if (Date.now() - startTime > timeout) {
logger.error('Timeout trying to connect to database:', lastError);
throw new Error(`Timeout trying to connect to database. Last error was: ${lastError.message}`);
}
await time.msleep(1000);
}
}
export async function connectDb(dbConfig: DatabaseConfig): Promise<DbConnection> {
return knex(makeKnexConfig(dbConfig));
}
export async function disconnectDb(db: DbConnection) {
await db.destroy();
}
export async function migrateDb(db: DbConnection) {
await db.migrate.latest({
directory: migrationDir,
// Disable transactions because the models might open one too
disableTransactions: true,
});
}
function allTableNames(): string[] {
const tableNames = Object.keys(databaseSchema);
tableNames.push('knex_migrations');
tableNames.push('knex_migrations_lock');
return tableNames;
}
export async function dropTables(db: DbConnection): Promise<void> {
for (const tableName of allTableNames()) {
try {
await db.schema.dropTable(tableName);
} catch (error) {
if (isNoSuchTableError(error)) continue;
throw error;
}
}
}
export async function truncateTables(db: DbConnection): Promise<void> {
for (const tableName of allTableNames()) {
try {
await db(tableName).truncate();
} catch (error) {
if (isNoSuchTableError(error)) continue;
throw error;
}
}
}
function isNoSuchTableError(error: any): boolean {
if (error) {
// Postgres error: 42P01: undefined_table
if (error.code === '42P01') return true;
// Sqlite3 error
if (error.message && error.message.includes('SQLITE_ERROR: no such table:')) return true;
}
return false;
}
export function isUniqueConstraintError(error: any): boolean {
if (error) {
// Postgres error: 23505: unique_violation
if (error.code === '23505') return true;
// Sqlite3 error
if (error.code === 'SQLITE_CONSTRAINT' && error.message.includes('UNIQUE constraint')) return true;
}
return false;
}
export async function latestMigration(db: DbConnection): Promise<any> {
try {
const result = await db('knex_migrations').select('name').orderBy('id', 'asc').first();
return result;
} catch (error) {
// If the database has never been initialized, we return null, so
// for this we need to check the error code, which will be
// different depending on the DBMS.
if (isNoSuchTableError(error)) return null;
throw error;
}
}
export async function connectionCheck(db: DbConnection): Promise<ConnectionCheckResult> {
try {
const result = await latestMigration(db);
return {
latestMigration: result,
isCreated: !!result,
error: null,
connection: db,
};
} catch (error) {
return {
latestMigration: null,
isCreated: false,
error: error,
connection: null,
};
}
}
export type Uuid = string;
export enum ItemAddressingType {
Id = 1,
Path,
}
export enum NotificationLevel {
Important = 10,
Normal = 20,
}
export enum ItemType {
Item = 1,
UserItem = 2,
User,
}
export enum ChangeType {
Create = 1,
Update = 2,
Delete = 3,
}
export enum FileContentType {
Any = 1,
JoplinItem = 2,
}
export function changeTypeToString(t: ChangeType): string {
if (t === ChangeType.Create) return 'create';
if (t === ChangeType.Update) return 'update';
if (t === ChangeType.Delete) return 'delete';
throw new Error(`Unkown type: ${t}`);
}
export enum ShareType {
Link = 1, // When a note is shared via a public link
App = 2, // When a note is shared with another user on the same server instance
JoplinRootFolder = 3,
}
export enum ShareUserStatus {
Waiting = 0,
Accepted = 1,
Rejected = 2,
}
export interface WithDates {
updated_time?: number;
created_time?: number;
}
export interface WithUuid {
id?: Uuid;
}
interface DatabaseTableColumn {
type: string;
}
interface DatabaseTable {
[key: string]: DatabaseTableColumn;
}
interface DatabaseTables {
[key: string]: DatabaseTable;
}
// AUTO-GENERATED-TYPES
// Auto-generated using `npm run generate-types`
export interface User extends WithDates, WithUuid {
email?: string;
password?: string;
full_name?: string;
is_admin?: number;
}
export interface Session extends WithDates, WithUuid {
user_id?: Uuid;
auth_code?: string;
}
export interface File {
id?: Uuid;
owner_id?: Uuid;
name?: string;
content?: any;
mime_type?: string;
size?: number;
is_directory?: number;
is_root?: number;
parent_id?: Uuid;
updated_time?: string;
created_time?: string;
source_file_id?: Uuid;
content_type?: number;
content_id?: Uuid;
}
export interface ApiClient extends WithDates, WithUuid {
name?: string;
secret?: string;
}
export interface Notification extends WithDates, WithUuid {
owner_id?: Uuid;
level?: NotificationLevel;
key?: string;
message?: string;
read?: number;
canBeDismissed?: number;
}
export interface ShareUser extends WithDates, WithUuid {
share_id?: Uuid;
user_id?: Uuid;
status?: ShareUserStatus;
}
export interface Item extends WithDates, WithUuid {
name?: string;
mime_type?: string;
content?: Buffer;
content_size?: number;
jop_id?: Uuid;
jop_parent_id?: Uuid;
jop_share_id?: Uuid;
jop_type?: number;
jop_encryption_applied?: number;
}
export interface UserItem extends WithDates {
id?: number;
user_id?: Uuid;
item_id?: Uuid;
}
export interface ItemResource {
id?: number;
item_id?: Uuid;
resource_id?: Uuid;
}
export interface KeyValue {
id?: number;
key?: string;
type?: number;
value?: string;
}
export interface Share extends WithDates, WithUuid {
owner_id?: Uuid;
item_id?: Uuid;
type?: ShareType;
folder_id?: Uuid;
note_id?: Uuid;
}
export interface Change extends WithDates, WithUuid {
counter?: number;
item_type?: ItemType;
item_id?: Uuid;
item_name?: string;
type?: ChangeType;
previous_item?: string;
user_id?: Uuid;
}
export const databaseSchema: DatabaseTables = {
users: {
id: { type: 'string' },
email: { type: 'string' },
password: { type: 'string' },
full_name: { type: 'string' },
is_admin: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
sessions: {
id: { type: 'string' },
user_id: { type: 'string' },
auth_code: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
files: {
id: { type: 'string' },
owner_id: { type: 'string' },
name: { type: 'string' },
content: { type: 'any' },
mime_type: { type: 'string' },
size: { type: 'number' },
is_directory: { type: 'number' },
is_root: { type: 'number' },
parent_id: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
source_file_id: { type: 'string' },
content_type: { type: 'number' },
content_id: { type: 'string' },
},
api_clients: {
id: { type: 'string' },
name: { type: 'string' },
secret: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
notifications: {
id: { type: 'string' },
owner_id: { type: 'string' },
level: { type: 'number' },
key: { type: 'string' },
message: { type: 'string' },
read: { type: 'number' },
canBeDismissed: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
share_users: {
id: { type: 'string' },
share_id: { type: 'string' },
user_id: { type: 'string' },
status: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
items: {
id: { type: 'string' },
name: { type: 'string' },
mime_type: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
content: { type: 'any' },
content_size: { type: 'number' },
jop_id: { type: 'string' },
jop_parent_id: { type: 'string' },
jop_share_id: { type: 'string' },
jop_type: { type: 'number' },
jop_encryption_applied: { type: 'number' },
},
user_items: {
id: { type: 'number' },
user_id: { type: 'string' },
item_id: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
item_resources: {
id: { type: 'number' },
item_id: { type: 'string' },
resource_id: { type: 'string' },
},
key_values: {
id: { type: 'number' },
key: { type: 'string' },
type: { type: 'number' },
value: { type: 'string' },
},
shares: {
id: { type: 'string' },
owner_id: { type: 'string' },
item_id: { type: 'string' },
type: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
folder_id: { type: 'string' },
note_id: { type: 'string' },
},
changes: {
counter: { type: 'number' },
id: { type: 'string' },
item_type: { type: 'number' },
item_id: { type: 'string' },
item_name: { type: 'string' },
type: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
previous_item: { type: 'string' },
user_id: { type: 'string' },
},
};
// AUTO-GENERATED-TYPES