├── .nvmrc ├── .gitignore ├── Gemfile ├── src ├── index.ts ├── crypto.ts ├── book.ts ├── validator.ts ├── tag.ts ├── file.ts └── note.ts ├── prettier.config.js ├── jest.config.js ├── docs ├── meta.json └── schema.md ├── compile_schema.sh ├── tsconfig.json ├── generate_doc.sh ├── __tests__ ├── tag.test.ts ├── book.test.ts ├── file.test.ts └── note.test.ts ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── eslint.config.mjs ├── README.md ├── yaml-schema ├── book.yaml ├── tag.yaml ├── file.yaml └── note.yaml ├── rollup.config.js └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | json-schema 3 | vendor 4 | validators 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'prmd' 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './note' 2 | export * from './book' 3 | export * from './tag' 4 | export * from './file' 5 | export * from './crypto' 6 | export * from './validator' 7 | -------------------------------------------------------------------------------- /src/crypto.ts: -------------------------------------------------------------------------------- 1 | export type EncryptionMetadata = { 2 | algorithm: string 3 | iv: string 4 | tag: string 5 | } 6 | export type EncryptedData = EncryptionMetadata & { 7 | content: string | Buffer 8 | } 9 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | arrowParens: 'avoid', 3 | singleQuote: true, 4 | bracketSpacing: true, 5 | endOfLine: 'lf', 6 | semi: false, 7 | tabWidth: 2, 8 | trailingComma: 'none' 9 | } 10 | 11 | module.exports = options 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.(t|j)sx?$': 'ts-jest' 4 | }, 5 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 6 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 7 | testEnvironment: 'node' 8 | } 9 | -------------------------------------------------------------------------------- /docs/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Inkdrop Model", 3 | "description": "Inkdrop data model definitions in json-schema and flowtype", 4 | "id": "inkdrop-model", 5 | "links": [{ 6 | "href": "https://www.inkdrop.app/", 7 | "rel": "self" 8 | }, { 9 | "href": "https://docs.inkdrop.app/", 10 | "rel": "documentations" 11 | }] 12 | } 13 | -------------------------------------------------------------------------------- /compile_schema.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | yaml2json -p -s yaml-schema/ 4 | mkdir -p json-schema 5 | 6 | for path in yaml-schema/*.json; do 7 | filename=$(basename $path) 8 | schemaName=${filename%.*} 9 | jq 'del(.properties[].example)' $path > json-schema/${filename} 10 | mkdir -p ./validators 11 | ajv compile -s json-schema/${filename} -o validators/${schemaName}.js 12 | done 13 | 14 | rm yaml-schema/*.json 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "strict": true, 9 | "declaration": true, 10 | "declarationDir": ".", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "noImplicitAny": false 14 | }, 15 | "include": ["src/index.ts"], 16 | "exclude": ["node_modules", "**/*.test.ts", "lib"] 17 | } 18 | -------------------------------------------------------------------------------- /generate_doc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | yaml2json -p -s yaml-schema/ 4 | 5 | # Rename key `$id` to `id` 6 | for path in yaml-schema/*.json; do 7 | filename=$(basename $path) 8 | schemaName=${filename%.*} 9 | 10 | mkdir -p ./json-schema-doc 11 | jq '.["id"] = .["$id"] | del(.["$id"])' $path > json-schema-doc/${filename} 12 | done 13 | 14 | bundle exec prmd combine --meta docs/meta.json json-schema-doc/ > schema.json 15 | bundle exec prmd doc schema.json > docs/schema.md 16 | 17 | rm schema.json 18 | rm yaml-schema/*.json 19 | rm -rf json-schema-doc 20 | -------------------------------------------------------------------------------- /src/book.ts: -------------------------------------------------------------------------------- 1 | import type { ValidateFunction } from 'ajv' 2 | import BookSchema from '../json-schema/book.json' 3 | import validator from '../validators/book' 4 | 5 | import type { EncryptedData } from './crypto' 6 | export type BookMetadata = { 7 | _id: string 8 | _rev?: string 9 | updatedAt: number 10 | createdAt: number 11 | count?: number 12 | parentBookId?: null | string 13 | migratedBy?: string 14 | } 15 | export type Book = BookMetadata & { 16 | name: string 17 | } 18 | export type EncryptedBook = BookMetadata & { 19 | encryptedData: EncryptedData 20 | } 21 | 22 | const validateBook: ValidateFunction = validator as any 23 | export { BookSchema, validateBook } 24 | -------------------------------------------------------------------------------- /src/validator.ts: -------------------------------------------------------------------------------- 1 | import type { ErrorObject } from 'ajv' 2 | 3 | export function validationErrorsToMessage(errors: ErrorObject[]): string { 4 | if (errors instanceof Array) { 5 | return errors 6 | .map(e => { 7 | if (typeof e === 'object') { 8 | return `"${e.instancePath}" ${e.message}` 9 | } else { 10 | return e 11 | } 12 | }) 13 | .join(', ') 14 | } else { 15 | return errors 16 | } 17 | } 18 | export class InvalidDataError extends Error { 19 | name = 'InvalidDataError' 20 | errors: ErrorObject[] 21 | 22 | constructor(message: string, errors: ErrorObject[]) { 23 | super(message + ' ' + validationErrorsToMessage(errors)) 24 | this.errors = errors 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /__tests__/tag.test.ts: -------------------------------------------------------------------------------- 1 | import type { Tag } from '../lib' 2 | import { TagSchema, validateTag } from '../lib' 3 | import Ajv from 'ajv' 4 | 5 | const ajv = new Ajv() 6 | let validate: any 7 | 8 | test('check schema', () => { 9 | validate = ajv.compile(TagSchema) 10 | expect(typeof validate).toBe('function') 11 | }) 12 | 13 | test('basic validation', () => { 14 | const data: Tag = { 15 | count: 3, 16 | color: 'green', 17 | createdAt: 1482130519215, 18 | updatedAt: 1493014639273, 19 | name: 'Admin', 20 | _id: 'tag:0ebd717b-ba17-49f7-a014-c89caea418f3', 21 | _rev: '5-caf95ffd831665119f6d4f01113cdab4' 22 | } 23 | const valid = validate(data) 24 | expect(valid).toBe(true) 25 | const valid2 = validateTag(data) 26 | expect(valid2).toBe(true) 27 | }) 28 | -------------------------------------------------------------------------------- /__tests__/book.test.ts: -------------------------------------------------------------------------------- 1 | import type { Book } from '../src' 2 | import { BookSchema, validateBook } from '../src' 3 | import Ajv from 'ajv' 4 | const ajv = new Ajv() 5 | let validate: any 6 | 7 | test('check schema', () => { 8 | validate = ajv.compile(BookSchema) 9 | expect(typeof validate).toBe('function') 10 | }) 11 | 12 | test('basic validation', () => { 13 | const data: Book = { 14 | updatedAt: 1494489037778, 15 | createdAt: 1494489037778, 16 | count: 6, 17 | parentBookId: null, 18 | name: 'Blog', 19 | migratedBy: 'migrateAddingParentBookId', 20 | _id: 'book:9dc6a7a7-a0e4-4eeb-997c-32b385767dc2', 21 | _rev: '7-04b06614a08feaab9add6fc2e909148a' 22 | } 23 | const valid = validate(data) 24 | expect(valid).toBe(true) 25 | const valid2 = validateBook(data) 26 | expect(valid2).toBe(true) 27 | }) 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [20.x] 12 | 13 | steps: 14 | - name: Checkout 🛎️ 15 | uses: actions/checkout@v4 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: Set up Ruby 3.2.0 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: 3.2 24 | bundler-cache: true 25 | 26 | - name: Install 🔧 27 | run: npm install 28 | 29 | - name: Build 🏗️ 30 | run: npm run build 31 | 32 | - name: Lint 🧐 33 | run: npm run lint 34 | 35 | - name: Test 🚨 36 | run: npm test 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Takuya Matsuyama 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals' 2 | import js from '@eslint/js' 3 | import tseslint from 'typescript-eslint' 4 | import prettier from 'eslint-config-prettier' 5 | 6 | export default [ 7 | js.configs.recommended, 8 | ...tseslint.configs.recommended, 9 | prettier, 10 | { ignores: ['lib', 'validators'] }, 11 | { 12 | languageOptions: { 13 | globals: { 14 | ...globals.node 15 | } 16 | }, 17 | 18 | rules: { 19 | 'no-useless-escape': 0, 20 | 'no-case-declarations': 0, 21 | 'prefer-const': 2, 22 | 'no-unused-vars': 0, 23 | 'no-unused-expressions': 0, 24 | 'no-constant-condition': [ 25 | 2, 26 | { 27 | checkLoops: false 28 | } 29 | ], 30 | 31 | '@typescript-eslint/no-explicit-any': 0, 32 | '@typescript-eslint/no-unused-vars': [ 33 | 2, 34 | { 35 | argsIgnorePattern: '^_', 36 | varsIgnorePattern: '^_', 37 | caughtErrorsIgnorePattern: '^_' 38 | } 39 | ], 40 | '@typescript-eslint/ban-ts-comment': 0, 41 | '@typescript-eslint/no-this-alias': 0, 42 | '@typescript-eslint/no-unused-expressions': 0 43 | } 44 | } 45 | ] 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Inkdrop Model 2 | ================== 3 | 4 | Inkdrop data model definitions in json-schema and TypeScript 5 | 6 | ## Documentations 7 | 8 | Human readable definitions are [here](https://github.com/inkdropapp/inkdrop-model/tree/master/docs/schema.md). 9 | 10 | ## Install 11 | 12 | ```sh 13 | npm install inkdrop-model 14 | ``` 15 | 16 | ## Usage 17 | 18 | ### TypeScript 19 | 20 | ```typescript 21 | import type { Note, Book, Tag, File } from 'inkdrop-model' 22 | ``` 23 | 24 | ### Json Schema 25 | 26 | ```javascript 27 | import { NoteSchema, BookSchema, TagSchema, FileSchema } from 'inkdrop-model' 28 | ``` 29 | 30 | You can validate data with json schemas. 31 | Below example uses [ajv](https://github.com/epoberezkin/ajv) as a validator: 32 | 33 | ```javascript 34 | import { NoteSchema } from 'inkdrop-model' 35 | import Ajv from 'ajv' 36 | const ajv = new Ajv() 37 | const validate = ajv.compile(NoteSchema) 38 | 39 | const data = { 40 | _id: 'note:BkgOZZUJzf', 41 | title: 'link', 42 | doctype: 'markdown', 43 | updatedAt: 1513330812556, 44 | createdAt: 1513214207639, 45 | tags: [], 46 | status: 'none', 47 | share: 'private', 48 | body: 'markdown note body', 49 | bookId: 'book:first', 50 | _rev: '38-636e505958d24f9c21614d95ea03b5a1' 51 | } 52 | const valid = validate(data) 53 | console.log(valid) // => true 54 | ``` 55 | 56 | -------------------------------------------------------------------------------- /yaml-schema/book.yaml: -------------------------------------------------------------------------------- 1 | $schema: 'http://json-schema.org/draft-07/schema#' 2 | $id: book 3 | title: Book 4 | description: 'A notebook data' 5 | type: object 6 | properties: 7 | _id: 8 | description: 'The unique notebook ID which should start with `book:` and the remains are randomly generated string' 9 | type: string 10 | minLength: 6 11 | maxLength: 128 12 | pattern: '^book:' 13 | example: 'book:9dc6a7a7' 14 | _rev: 15 | description: 'This is a CouchDB specific field. The current MVCC-token/revision of this document (mandatory and immutable)' 16 | type: string 17 | example: 14-813af5085bb6a2648c3f0aca37fc821f 18 | name: 19 | description: 'The notebook name' 20 | type: string 21 | minLength: 1 22 | maxLength: 64 23 | updatedAt: 24 | description: 'The date time when the notebook was last updated, represented with Unix timestamps in milliseconds' 25 | type: number 26 | createdAt: 27 | description: 'The date time when the notebook was created, represented with Unix timestamps in milliseconds' 28 | type: number 29 | count: 30 | description: 'It indicates the number of notes in the notebook' 31 | type: number 32 | parentBookId: 33 | description: 'The ID of the parent notebook' 34 | type: [string, 'null'] 35 | required: 36 | - _id 37 | - name 38 | - updatedAt 39 | - createdAt 40 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import packageJson from './package.json' 3 | import typescript from 'rollup-plugin-typescript2' 4 | import terser from '@rollup/plugin-terser' 5 | import json from '@rollup/plugin-json' 6 | import localTypescript from 'typescript' 7 | 8 | const CONFIG_TYPESCRIPT = { 9 | tsconfig: 'tsconfig.json', 10 | typescript: localTypescript 11 | } 12 | 13 | const kebabCaseToPascalCase = (string = '') => { 14 | return string.replace(/(^\w|-\w)/g, replaceString => 15 | replaceString.replace(/-/, '').toUpperCase() 16 | ) 17 | } 18 | 19 | const baseName = path.join('lib', 'index') 20 | 21 | export default [ 22 | { 23 | input: 'src/index.ts', 24 | output: [ 25 | { 26 | file: `${baseName}.js`, 27 | format: 'cjs', 28 | strict: true, 29 | sourcemap: true, 30 | exports: 'auto' 31 | }, 32 | { 33 | file: `${baseName}.esm.js`, 34 | format: 'esm', 35 | strict: true, 36 | sourcemap: true 37 | }, 38 | { 39 | file: `${baseName}.umd.js`, 40 | format: 'umd', 41 | strict: true, 42 | sourcemap: false, 43 | name: kebabCaseToPascalCase(packageJson.name), 44 | plugins: [terser()] 45 | } 46 | ], 47 | external: [ 48 | '../validators/note', 49 | '../validators/book', 50 | '../validators/tag', 51 | '../validators/file' 52 | ], 53 | plugins: [json(), typescript(CONFIG_TYPESCRIPT)] 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /src/tag.ts: -------------------------------------------------------------------------------- 1 | import type { ValidateFunction } from 'ajv' 2 | import TagSchema from '../json-schema/tag.json' 3 | import validator from '../validators/tag' 4 | import type { EncryptedData } from './crypto' 5 | export type TagColor = 6 | | 'default' 7 | | 'red' 8 | | 'orange' 9 | | 'yellow' 10 | | 'olive' 11 | | 'green' 12 | | 'teal' 13 | | 'blue' 14 | | 'violet' 15 | | 'purple' 16 | | 'pink' 17 | | 'brown' 18 | | 'grey' 19 | | 'black' 20 | export const TAG_COLOR: Readonly<{ 21 | DEFAULT: 'default' 22 | RED: 'red' 23 | ORANGE: 'orange' 24 | YELLOW: 'yellow' 25 | OLIVE: 'olive' 26 | GREEN: 'green' 27 | TEAL: 'teal' 28 | BLUE: 'blue' 29 | VIOLET: 'violet' 30 | PURPLE: 'purple' 31 | PINK: 'pink' 32 | BROWN: 'brown' 33 | GREY: 'grey' 34 | BLACK: 'black' 35 | }> = { 36 | DEFAULT: 'default', 37 | RED: 'red', 38 | ORANGE: 'orange', 39 | YELLOW: 'yellow', 40 | OLIVE: 'olive', 41 | GREEN: 'green', 42 | TEAL: 'teal', 43 | BLUE: 'blue', 44 | VIOLET: 'violet', 45 | PURPLE: 'purple', 46 | PINK: 'pink', 47 | BROWN: 'brown', 48 | GREY: 'grey', 49 | BLACK: 'black' 50 | } 51 | export type TagMetadata = { 52 | _id: string 53 | _rev?: string 54 | count?: number 55 | color: TagColor 56 | updatedAt: number 57 | createdAt: number 58 | } 59 | export type Tag = TagMetadata & { 60 | name: string 61 | } 62 | export type EncryptedTag = TagMetadata & { 63 | encryptedData: EncryptedData 64 | } 65 | const validateTag: ValidateFunction = validator as any 66 | export { TagSchema, validateTag } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inkdrop-model", 3 | "version": "2.9.1", 4 | "description": "Data model for Inkdrop", 5 | "scripts": { 6 | "build": "npm-run-all build:schema build:lib doc", 7 | "build:lib": "rollup -c --bundleConfigAsCjs && mv lib/src/* lib/ && rm -r lib/src", 8 | "build:schema": "./compile_schema.sh", 9 | "lint": "eslint src __tests__", 10 | "test": "jest --config jest.config.js", 11 | "doc": "./generate_doc.sh", 12 | "prepublishOnly": "npm-run-all build:* lint test" 13 | }, 14 | "author": "Takuya Matsuyama ", 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/inkdropapp/inkdrop-model.git" 19 | }, 20 | "devDependencies": { 21 | "@rollup/plugin-json": "^6.1.0", 22 | "@rollup/plugin-terser": "^0.4.4", 23 | "@types/jest": "^30.0.0", 24 | "ajv": "^8.17.1", 25 | "ajv-cli": "^5.0.0", 26 | "eslint": "^9.39.1", 27 | "eslint-config-prettier": "^10.1.8", 28 | "jest": "^30.2.0", 29 | "npm-run-all": "^4.1.5", 30 | "prettier": "^3.7.4", 31 | "rollup": "^4.53.3", 32 | "rollup-plugin-typescript2": "^0.36.0", 33 | "ts-jest": "^29.4.6", 34 | "typescript": "^5.9.3", 35 | "typescript-eslint": "^8.48.1", 36 | "yamljs": "^0.3.0" 37 | }, 38 | "types": "lib/index.d.ts", 39 | "main": "lib/index.js", 40 | "unpkg": "lib/index.umd.js", 41 | "module": "lib/index.esm.js", 42 | "keywords": [ 43 | "json-schema" 44 | ], 45 | "files": [ 46 | "lib", 47 | "json-schema", 48 | "validators" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /yaml-schema/tag.yaml: -------------------------------------------------------------------------------- 1 | $schema: 'http://json-schema.org/draft-07/schema#' 2 | $id: tag 3 | title: Tag 4 | description: 'A note tag' 5 | type: object 6 | properties: 7 | _id: 8 | description: 'The unique tag ID which should start with `tag:` and the remains are randomly generated string' 9 | type: string 10 | minLength: 6 11 | maxLength: 128 12 | pattern: '^tag:' 13 | example: 'tag:0ebd717b' 14 | _rev: 15 | description: 'This is a CouchDB specific field. The current MVCC-token/revision of this document (mandatory and immutable)' 16 | type: string 17 | example: 14-813af5085bb6a2648c3f0aca37fc821f 18 | name: 19 | description: 'The name of the tag' 20 | type: string 21 | maxLength: 64 22 | count: 23 | description: 'It indicates the number of notes with the tag' 24 | type: number 25 | color: 26 | description: 'The color type of the tag' 27 | type: string 28 | enum: 29 | - default 30 | - red 31 | - orange 32 | - yellow 33 | - olive 34 | - green 35 | - teal 36 | - blue 37 | - violet 38 | - purple 39 | - pink 40 | - brown 41 | - grey 42 | - black 43 | updatedAt: 44 | description: 'The date time when the tag was last updated, represented with Unix timestamps in milliseconds' 45 | type: number 46 | createdAt: 47 | description: 'The date time when the tag was created, represented with Unix timestamps in milliseconds' 48 | type: number 49 | required: 50 | - _id 51 | - name 52 | - count 53 | - updatedAt 54 | - createdAt 55 | -------------------------------------------------------------------------------- /src/file.ts: -------------------------------------------------------------------------------- 1 | import type { ValidateFunction } from 'ajv' 2 | import FileSchema from '../json-schema/file.json' 3 | import validator from '../validators/file' 4 | import type { EncryptionMetadata } from './crypto' 5 | export type ImageFileType = 6 | | 'image/png' 7 | | 'image/jpeg' 8 | | 'image/jpg' 9 | | 'image/svg+xml' 10 | | 'image/gif' 11 | | 'image/heic' 12 | | 'image/heif' 13 | export const supportedImageFileTypes: ReadonlyArray = [ 14 | 'image/png', 15 | 'image/jpeg', 16 | 'image/jpg', 17 | 'image/svg+xml', 18 | 'image/gif', 19 | 'image/heic', 20 | 'image/heif' 21 | ] 22 | export const SUPPORTED_IMAGE_MIME_TYPES: { 23 | readonly [mime: string]: ImageFileType 24 | } = { 25 | ...supportedImageFileTypes.reduce( 26 | (hash, ft) => ({ ...hash, [ft.split('/')[1]]: ft }), 27 | {} as Record 28 | ), 29 | jpg: 'image/jpeg' 30 | } 31 | export const maxAttachmentFileSize: number = 10 * 1024 * 1024 32 | export type FileAttachmentItem = { 33 | digest?: string 34 | content_type: ImageFileType 35 | data: Buffer | string 36 | length?: number 37 | } 38 | export type File = { 39 | _id: string 40 | _rev?: string 41 | name: string 42 | createdAt: number 43 | contentType: ImageFileType 44 | contentLength: number 45 | publicIn: string[] 46 | _attachments: { 47 | index: FileAttachmentItem 48 | } 49 | md5digest?: string 50 | } 51 | export type EncryptedFile = File & { 52 | encryptionData: EncryptionMetadata 53 | } 54 | const validateFile: ValidateFunction = validator as any 55 | export { FileSchema, validateFile } 56 | -------------------------------------------------------------------------------- /src/note.ts: -------------------------------------------------------------------------------- 1 | import type { ValidateFunction } from 'ajv' 2 | import NoteSchema from '../json-schema/note.json' 3 | import validator from '../validators/note' 4 | import type { EncryptedData } from './crypto' 5 | export type TrashBookId = 'trash' 6 | export type NoteStatus = 'none' | 'active' | 'onHold' | 'completed' | 'dropped' 7 | export type NoteVisibility = 'private' | 'public' 8 | export type NoteMetadata = { 9 | _id: string 10 | _rev?: string 11 | bookId: string 12 | doctype: string 13 | updatedAt: number 14 | createdAt: number 15 | tags?: string[] 16 | numOfTasks?: number 17 | numOfCheckedTasks?: number 18 | migratedBy?: string 19 | status?: NoteStatus 20 | share?: NoteVisibility 21 | pinned?: boolean 22 | timestamp: number 23 | _conflicts?: string[] 24 | } 25 | export type Note = NoteMetadata & { 26 | title: string 27 | body: string 28 | } 29 | export type EncryptedNote = NoteMetadata & { 30 | encryptedData: EncryptedData 31 | } 32 | export const TRASH_BOOK_ID = 'trash' 33 | 34 | export const NOTE_STATUS: Readonly<{ 35 | NONE: 'none' 36 | ACTIVE: 'active' 37 | ON_HOLD: 'onHold' 38 | COMPLETED: 'completed' 39 | DROPPED: 'dropped' 40 | }> = { 41 | NONE: 'none', 42 | ACTIVE: 'active', 43 | ON_HOLD: 'onHold', 44 | COMPLETED: 'completed', 45 | DROPPED: 'dropped' 46 | } 47 | export const NOTE_VISIBILITY: Readonly<{ 48 | PRIVATE: 'private' 49 | PUBLIC: 'public' 50 | }> = { 51 | PRIVATE: 'private', 52 | PUBLIC: 'public' 53 | } 54 | const validateNote: ValidateFunction = validator as any 55 | export { NoteSchema, validateNote } 56 | 57 | export const NOTE_TITLE_MAX_LENGTH: number = 256 58 | -------------------------------------------------------------------------------- /__tests__/file.test.ts: -------------------------------------------------------------------------------- 1 | import type { File } from '../src' 2 | import { FileSchema, validateFile, validationErrorsToMessage } from '../src' 3 | import Ajv from 'ajv' 4 | const ajv = new Ajv({ allowUnionTypes: true }) 5 | let validate: any 6 | 7 | test('check schema', () => { 8 | validate = ajv.compile(FileSchema) 9 | expect(typeof validate).toBe('function') 10 | }) 11 | 12 | test('basic validation', () => { 13 | const data: File = { 14 | createdAt: 1503124076660, 15 | name: 'Dog.jpg', 16 | contentType: 'image/jpeg', 17 | contentLength: 13879, 18 | publicIn: ['note:Bkl_9Vubx'], 19 | _attachments: { 20 | index: { 21 | digest: 'md5-j+1LGuZoOdF1G5EFuv8+xA==', 22 | content_type: 'image/jpeg', 23 | data: 'data', 24 | length: 13879 25 | } 26 | }, 27 | _id: 'file:By8_nQtce', 28 | _rev: '14-813af5085bb6a2648c3f0aca37fc821f' 29 | } 30 | const valid = validate(data) 31 | expect(valid).toBe(true) 32 | const valid2 = validateFile(data) 33 | expect(valid2).toBe(true) 34 | }) 35 | 36 | test('too large image', () => { 37 | const data: File = { 38 | createdAt: 1503124076660, 39 | name: 'Dog.jpg', 40 | contentType: 'image/jpeg', 41 | contentLength: 1503124076660, 42 | publicIn: ['note:Bkl_9Vubx'], 43 | _attachments: { 44 | index: { 45 | digest: 'md5-j+1LGuZoOdF1G5EFuv8+xA==', 46 | content_type: 'image/jpeg', 47 | data: 'data', 48 | length: 1503124076660 49 | } 50 | }, 51 | _id: 'file:By8_nQtce', 52 | _rev: '14-813af5085bb6a2648c3f0aca37fc821f' 53 | } 54 | const valid = validate(data) 55 | expect(valid).toBe(false) 56 | expect(validate.errors).toEqual([ 57 | { 58 | instancePath: '/contentLength', 59 | keyword: 'maximum', 60 | message: 'must be <= 10485760', 61 | params: { 62 | comparison: '<=', 63 | limit: 10485760 64 | }, 65 | schemaPath: '#/properties/contentLength/maximum' 66 | } 67 | ]) 68 | const valid2 = validateFile(data) 69 | expect(valid2).toBe(false) 70 | const { errors } = validateFile 71 | expect(typeof errors).toBe('object') 72 | if (errors) { 73 | const errmsg = validationErrorsToMessage(errors) 74 | expect(errmsg).toBe('"/contentLength" must be <= 10485760') 75 | } 76 | }) 77 | -------------------------------------------------------------------------------- /yaml-schema/file.yaml: -------------------------------------------------------------------------------- 1 | $schema: 'http://json-schema.org/draft-07/schema#' 2 | $id: file 3 | title: File 4 | description: 'An attachment file' 5 | type: object 6 | properties: 7 | _id: 8 | description: 'The unique document ID which should start with `file:` and the remains are randomly generated string' 9 | type: string 10 | minLength: 6 11 | maxLength: 128 12 | pattern: '^file:' 13 | example: 'file:By8_nQtce' 14 | _rev: 15 | description: 'This is a CouchDB specific field. The current MVCC-token/revision of this document (mandatory and immutable).' 16 | type: string 17 | example: 14-813af5085bb6a2648c3f0aca37fc821f 18 | name: 19 | description: 'The file name' 20 | type: string 21 | minLength: 1 22 | maxLength: 128 23 | createdAt: 24 | description: 'The date time when the note was created, represented with Unix timestamps in milliseconds' 25 | type: number 26 | contentType: 27 | description: 'The MIME type of the content' 28 | type: string 29 | enum: 30 | - image/png 31 | - image/jpeg 32 | - image/jpg 33 | - image/svg+xml 34 | - image/gif 35 | - image/heic 36 | - image/heif 37 | maxLength: 128 38 | contentLength: 39 | description: 'The content length of the file' 40 | type: number 41 | maximum: 10485760 42 | publicIn: 43 | description: 'An array of the note IDs where the file is included' 44 | type: array 45 | items: 46 | type: string 47 | uniqueItems: true 48 | _attachments: 49 | description: 'The attachment file' 50 | type: object 51 | properties: 52 | index: 53 | description: 'The attachment file' 54 | type: object 55 | properties: 56 | content_type: 57 | description: 'The content type of the file' 58 | type: string 59 | enum: 60 | - image/png 61 | - image/jpeg 62 | - image/jpg 63 | - image/svg+xml 64 | - image/gif 65 | - image/heic 66 | - image/heif 67 | data: 68 | description: 'The file data' 69 | type: [ string, object ] 70 | required: 71 | - content_type 72 | - data 73 | required: 74 | - index 75 | required: 76 | - _id 77 | - name 78 | - createdAt 79 | - contentType 80 | - contentLength 81 | - publicIn 82 | - _attachments 83 | -------------------------------------------------------------------------------- /__tests__/note.test.ts: -------------------------------------------------------------------------------- 1 | import type { Note } from '../lib' 2 | import { NoteSchema, TRASH_BOOK_ID, validateNote } from '../src' 3 | import Ajv from 'ajv' 4 | 5 | const ajv = new Ajv() 6 | let validate: any 7 | 8 | test('check schema', () => { 9 | validate = ajv.compile(NoteSchema) 10 | expect(typeof validate).toBe('function') 11 | }) 12 | 13 | test('basic validation', () => { 14 | const data: Note = { 15 | _id: 'note:BkgOZZUJzf', 16 | title: 'link', 17 | doctype: 'markdown', 18 | updatedAt: 1513330812556, 19 | createdAt: 1513214207639, 20 | tags: ['tag:a28ca207'], 21 | status: 'none', 22 | share: 'private', 23 | body: 'markdown note body', 24 | bookId: 'book:first', 25 | numOfTasks: 0, 26 | numOfCheckedTasks: 0, 27 | pinned: false, 28 | _rev: '38-636e505958d24f9c21614d95ea03b5a1', 29 | timestamp: 1513330812556 30 | } 31 | const valid = validate(data) 32 | expect(valid).toBe(true) 33 | expect(validate.errors).toBe(null) 34 | const valid2 = validateNote(data) 35 | expect(valid2).toBe(true) 36 | }) 37 | 38 | test('failure validation', () => { 39 | const data: Record = { 40 | _id: 'invalid-note:BkgOZZUJzf', 41 | title: 0, 42 | doctype: 'not-markdown', 43 | updatedAt: 'a', 44 | createdAt: 'b', 45 | tags: ['tag:a28ca207'], 46 | status: 'none', 47 | share: 'private', 48 | body: 'markdown note body', 49 | bookId: 'book:first', 50 | numOfTasks: 0, 51 | numOfCheckedTasks: 0, 52 | pinned: true, 53 | timestamp: 1513330812556, 54 | _rev: '38-636e505958d24f9c21614d95ea03b5a1' 55 | } 56 | validateNote(data) 57 | expect(validateNote.errors).toEqual([ 58 | { 59 | instancePath: '/_id', 60 | keyword: 'pattern', 61 | message: 'must match pattern "^note:"', 62 | params: { 63 | pattern: '^note:' 64 | }, 65 | schemaPath: '#/properties/_id/pattern' 66 | } 67 | ]) 68 | }) 69 | 70 | test('trashed note', () => { 71 | const data: Note = { 72 | _id: 'note:BkgOZZUJzf', 73 | title: 'link', 74 | doctype: 'markdown', 75 | updatedAt: 1513330812556, 76 | createdAt: 1513214207639, 77 | tags: ['tag:a28ca207'], 78 | status: 'none', 79 | share: 'private', 80 | body: 'markdown note body', 81 | bookId: TRASH_BOOK_ID, 82 | numOfTasks: 0, 83 | numOfCheckedTasks: 0, 84 | timestamp: 1513330812556, 85 | _rev: '38-636e505958d24f9c21614d95ea03b5a1' 86 | } 87 | validate(data) 88 | expect(validate.errors).toBe(null) 89 | }) 90 | -------------------------------------------------------------------------------- /yaml-schema/note.yaml: -------------------------------------------------------------------------------- 1 | $schema: 'http://json-schema.org/draft-07/schema#' 2 | $id: note 3 | title: Note 4 | description: 'A note data' 5 | type: object 6 | properties: 7 | _id: 8 | description: 'The unique document ID which should start with `note:` and the remains are randomly generated string' 9 | type: string 10 | minLength: 6 11 | maxLength: 128 12 | pattern: '^note:' 13 | example: 'note:Bkl_9Vubx' 14 | _rev: 15 | description: 'This is a CouchDB specific field. The current MVCC-token/revision of this document (mandatory and immutable).' 16 | type: string 17 | example: 14-813af5085bb6a2648c3f0aca37fc821f 18 | bookId: 19 | description: 'The notebook ID' 20 | type: string 21 | minLength: 5 22 | maxLength: 128 23 | pattern: '^(book:|trash$)' 24 | title: 25 | description: 'The note title' 26 | type: string 27 | maxLength: 256 28 | doctype: 29 | description: 'The format type of the body field. It currently can take markdown only, reserved for the future' 30 | type: string 31 | enum: 32 | - markdown 33 | body: 34 | description: 'The content of the note represented with Markdown' 35 | type: string 36 | maxLength: 1048576 37 | updatedAt: 38 | description: 'The date time when the note was last updated, represented with Unix timestamps in milliseconds' 39 | type: number 40 | example: 1513330812556 41 | createdAt: 42 | description: 'The date time when the note was created, represented with Unix timestamps in milliseconds' 43 | type: number 44 | example: 1513330812556 45 | tags: 46 | description: 'The list of tag IDs' 47 | type: array 48 | items: 49 | type: string 50 | uniqueItems: true 51 | example: 52 | - 'tag:a28ca207' 53 | numOfTasks: 54 | description: 'The number of tasks, extracted from body' 55 | type: number 56 | numOfCheckedTasks: 57 | description: 'The number of checked tasks, extracted from body' 58 | type: number 59 | migratedBy: 60 | description: 'The type of the data migration' 61 | type: string 62 | example: migrateAddingParentBookId 63 | maxLength: 128 64 | status: 65 | description: 'The status of the note' 66 | type: string 67 | enum: 68 | - none 69 | - active 70 | - onHold 71 | - completed 72 | - dropped 73 | share: 74 | description: 'The sharing mode of the note' 75 | type: string 76 | enum: 77 | - private 78 | - public 79 | pinned: 80 | description: 'Whether the note is pinned to top' 81 | type: boolean 82 | timestamp: 83 | description: 'The date time when the revision was written, represented with Unix timestamps in milliseconds' 84 | type: number 85 | example: 1513330812556 86 | _conflicts: 87 | description: 'Conflicted revisions' 88 | type: array 89 | items: 90 | type: string 91 | uniqueItems: true 92 | example: 93 | - '24-530ea621fb9b5b28b8ff4243e4235f01' 94 | required: 95 | - _id 96 | - bookId 97 | - title 98 | - doctype 99 | - body 100 | - updatedAt 101 | - createdAt 102 | - timestamp 103 | -------------------------------------------------------------------------------- /docs/schema.md: -------------------------------------------------------------------------------- 1 | 2 | ## Book 3 | 4 | 5 | A notebook data 6 | 7 | 8 | ### Attributes 9 | 10 |
11 | Details 12 | 13 | 14 | | Name | Type | Description | Example | 15 | | ------- | ------- | ------- | ------- | 16 | | **_id** | *string* | The unique notebook ID which should start with `book:` and the remains are randomly generated string
**pattern:**
^book:

**Length:** `6..128` | `"book:9dc6a7a7"` | 17 | | **_rev** | *string* | This is a CouchDB specific field. The current MVCC-token/revision of this document (mandatory and immutable) | `"14-813af5085bb6a2648c3f0aca37fc821f"` | 18 | | **count** | *number* | It indicates the number of notes in the notebook | `42.0` | 19 | | **createdAt** | *number* | The date time when the notebook was created, represented with Unix timestamps in milliseconds | `42.0` | 20 | | **name** | *string* | The notebook name
**Length:** `1..64` | `"example"` | 21 | | **parentBookId** | *nullable string* | The ID of the parent notebook | `null` | 22 | | **updatedAt** | *number* | The date time when the notebook was last updated, represented with Unix timestamps in milliseconds | `42.0` | 23 | 24 |
25 | 26 | 27 | 28 | ## File 29 | 30 | 31 | An attachment file 32 | 33 | 34 | ### Attributes 35 | 36 |
37 | Details 38 | 39 | 40 | | Name | Type | Description | Example | 41 | | ------- | ------- | ------- | ------- | 42 | | **_attachments:index:content_type** | *string* | The content type of the file
**one of:**`"image/png"` or `"image/jpeg"` or `"image/jpg"` or `"image/svg+xml"` or `"image/gif"` or `"image/heic"` or `"image/heif"` | `"image/png"` | 43 | | **_attachments:index:data** | *string or object* | The file data | | 44 | | **_id** | *string* | The unique document ID which should start with `file:` and the remains are randomly generated string
**pattern:**
^file:

**Length:** `6..128` | `"file:By8_nQtce"` | 45 | | **_rev** | *string* | This is a CouchDB specific field. The current MVCC-token/revision of this document (mandatory and immutable). | `"14-813af5085bb6a2648c3f0aca37fc821f"` | 46 | | **contentLength** | *number* | The content length of the file
**Range:** `value <= 10485760` | `42.0` | 47 | | **contentType** | *string* | The MIME type of the content
**one of:**`"image/png"` or `"image/jpeg"` or `"image/jpg"` or `"image/svg+xml"` or `"image/gif"` or `"image/heic"` or `"image/heif"`
**Length:** `0..128` | `"image/png"` | 48 | | **createdAt** | *number* | The date time when the note was created, represented with Unix timestamps in milliseconds | `42.0` | 49 | | **name** | *string* | The file name
**Length:** `1..128` | `"example"` | 50 | | **publicIn** | *array* | An array of the note IDs where the file is included | `[null]` | 51 | 52 |
53 | 54 | 55 | 56 | ## Note 57 | 58 | 59 | A note data 60 | 61 | 62 | ### Attributes 63 | 64 |
65 | Details 66 | 67 | 68 | | Name | Type | Description | Example | 69 | | ------- | ------- | ------- | ------- | 70 | | **_conflicts** | *array* | Conflicted revisions | `["24-530ea621fb9b5b28b8ff4243e4235f01"]` | 71 | | **_id** | *string* | The unique document ID which should start with `note:` and the remains are randomly generated string
**pattern:**
^note:

**Length:** `6..128` | `"note:Bkl_9Vubx"` | 72 | | **_rev** | *string* | This is a CouchDB specific field. The current MVCC-token/revision of this document (mandatory and immutable). | `"14-813af5085bb6a2648c3f0aca37fc821f"` | 73 | | **body** | *string* | The content of the note represented with Markdown
**Length:** `0..1048576` | `"example"` | 74 | | **bookId** | *string* | The notebook ID
**pattern:**
^(book:|trash$)

**Length:** `5..128` | `"example"` | 75 | | **createdAt** | *number* | The date time when the note was created, represented with Unix timestamps in milliseconds | `1513330812556` | 76 | | **doctype** | *string* | The format type of the body field. It currently can take markdown only, reserved for the future
**one of:**`"markdown"` | `"markdown"` | 77 | | **migratedBy** | *string* | The type of the data migration
**Length:** `0..128` | `"migrateAddingParentBookId"` | 78 | | **numOfCheckedTasks** | *number* | The number of checked tasks, extracted from body | `42.0` | 79 | | **numOfTasks** | *number* | The number of tasks, extracted from body | `42.0` | 80 | | **pinned** | *boolean* | Whether the note is pinned to top | `true` | 81 | | **share** | *string* | The sharing mode of the note
**one of:**`"private"` or `"public"` | `"private"` | 82 | | **status** | *string* | The status of the note
**one of:**`"none"` or `"active"` or `"onHold"` or `"completed"` or `"dropped"` | `"none"` | 83 | | **tags** | *array* | The list of tag IDs | `["tag:a28ca207"]` | 84 | | **timestamp** | *number* | The date time when the revision was written, represented with Unix timestamps in milliseconds | `1513330812556` | 85 | | **title** | *string* | The note title
**Length:** `0..256` | `"example"` | 86 | | **updatedAt** | *number* | The date time when the note was last updated, represented with Unix timestamps in milliseconds | `1513330812556` | 87 | 88 |
89 | 90 | 91 | 92 | ## Tag 93 | 94 | 95 | A note tag 96 | 97 | 98 | ### Attributes 99 | 100 |
101 | Details 102 | 103 | 104 | | Name | Type | Description | Example | 105 | | ------- | ------- | ------- | ------- | 106 | | **_id** | *string* | The unique tag ID which should start with `tag:` and the remains are randomly generated string
**pattern:**
^tag:

**Length:** `6..128` | `"tag:0ebd717b"` | 107 | | **_rev** | *string* | This is a CouchDB specific field. The current MVCC-token/revision of this document (mandatory and immutable) | `"14-813af5085bb6a2648c3f0aca37fc821f"` | 108 | | **color** | *string* | The color type of the tag
**one of:**`"default"` or `"red"` or `"orange"` or `"yellow"` or `"olive"` or `"green"` or `"teal"` or `"blue"` or `"violet"` or `"purple"` or `"pink"` or `"brown"` or `"grey"` or `"black"` | `"default"` | 109 | | **count** | *number* | It indicates the number of notes with the tag | `42.0` | 110 | | **createdAt** | *number* | The date time when the tag was created, represented with Unix timestamps in milliseconds | `42.0` | 111 | | **name** | *string* | The name of the tag
**Length:** `0..64` | `"example"` | 112 | | **updatedAt** | *number* | The date time when the tag was last updated, represented with Unix timestamps in milliseconds | `42.0` | 113 | 114 |
115 | 116 | 117 | --------------------------------------------------------------------------------