├── .circleci └── config.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── CNAME ├── README.md ├── example.ts ├── jest.config.js ├── logo.png ├── logo.sketch ├── package-lock.json ├── package.json ├── policy.png ├── src ├── db.ts ├── errors.ts ├── index.ts ├── storage.ts ├── types.ts └── utils │ └── error.ts ├── tests ├── main-test.ts └── public-test.ts ├── tsconfig.json └── tslint.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | node: circleci/node@1.1.6 4 | jobs: 5 | build-and-test: 6 | executor: 7 | name: node/default 8 | steps: 9 | - checkout 10 | - node/with-cache: 11 | steps: 12 | - run: npm install 13 | - run: npm build 14 | workflows: 15 | build-and-test: 16 | jobs: 17 | - build-and-test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | google-serviceaccount.json 2 | 3 | dist 4 | node_modules 5 | build 6 | test 7 | src/**.js 8 | .idea/* 9 | 10 | coverage 11 | .nyc_output 12 | *.log 13 | 14 | yarn.lock 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests 2 | jest.config.js 3 | .*.swp 4 | ._* 5 | .DS_Store 6 | .git 7 | .hg 8 | .npmrc 9 | .lock-wscript 10 | .svn 11 | .wafpickle-* 12 | config.gypi 13 | CVS 14 | npm-debug.log -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "semi": false, 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | sheetsql.joway.io -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SheetSQL 2 | 3 | 4 | 5 | [![npm](https://img.shields.io/npm/v/sheetsql.svg)](https://www.npmjs.com/package/sheetsql) 6 | [![CircleCI](https://circleci.com/gh/joway/sheetsql.svg?style=shield)](https://circleci.com/gh/joway/sheetsql) 7 | 8 | Google Spreadsheet as a Database. 9 | 10 | ## Purpose 11 | 12 | In the past, I often asked by non-technical colleagues to do some DB scripts jobs to mapping their spreadsheets data to the production database. And when their data changes the same work needs to be done again. Since these data are not changed too frequently(compared with other online data), it's also not worth to make a content management system for them. 13 | 14 | But why don't make their spreadsheets as a real production database? That's what ["Single source of truth"](https://en.wikipedia.org/wiki/Single_source_of_truth) means. What's more, you even could write back some statistical data like "Page View" to the spreadsheets, so they could see the feedback clearly and continue to optimize the content. 15 | 16 | ## Requirements 17 | 18 | 1. Create a Google Spreadsheet and populate the first row with the columns names, here is an [Example Sheet](https://docs.google.com/spreadsheets/d/1ya2Tl2ev9M80xYwspv7FJaoWq0oVOMBk3VF0f0MXv2s/edit?usp=sharing). 19 | 2. Create a [Google Cloud Service Account](https://cloud.google.com/docs/authentication/production) and download the JSON file that contains your key. 20 | 3. Find your service account email in [credentials console](https://console.cloud.google.com/apis/credentials) which similar with `account-name@project-name.iam.gserviceaccount.com`. 21 | 4. Share your sheets to the above email, and make sure you have assigned it as an editor. 22 | 23 | ![](./policy.png) 24 | 25 | ## Usage 26 | 27 | ### Concepts 28 | 29 | #### db 30 | 31 | `db` means the Google Spreadsheet ID. You can find it in your sheet's URL: `https://docs.google.com/spreadsheets/d/${YOUR_SHEETS_ID}/edit` 32 | 33 | #### table 34 | 35 | `table` means the Sheet Name in your Spreadsheet. The default is `Sheet1`. 36 | 37 | #### data type 38 | 39 | Every data in sheetsql will be set/get as a string. You need to handle the type mapping on your side. 40 | 41 | #### keyFile 42 | 43 | Your service account JSON key file. 44 | 45 | ### Install 46 | 47 | ``` 48 | npm i sheetsql -S 49 | ``` 50 | 51 | ### Example 52 | 53 | ```typescript 54 | const db = new Database({ 55 | db: '1ya2Tl2ev9M80xYwspv7FJaoWq0oVOMBk3VF0f0MXv2s', 56 | table: 'Sheet1', // optional, default = Sheet1 57 | keyFile: './google-serviceaccount.json', 58 | cacheTimeoutMs: 5000, // optional, default = 5000 59 | }) 60 | 61 | // load schema and data from google spreadsheet 62 | await db.load() 63 | 64 | // insert multiple documents 65 | let docs = await db.insert([ 66 | { 67 | name: 'joway', 68 | age: 18, 69 | }, 70 | ]) 71 | 72 | // find documents and update them 73 | docs = await db.update( 74 | { 75 | name: 'joway', 76 | }, 77 | { 78 | age: 100, 79 | }, 80 | ) 81 | 82 | // find documents 83 | docs = await db.find({ 84 | name: 'joway', 85 | }) 86 | 87 | // find all documents 88 | docs = await db.find({}) 89 | 90 | // find documents and remove them 91 | docs = await db.remove({ 92 | name: 'joway', 93 | }) 94 | ``` 95 | 96 | ### Using a Proxy 97 | 98 | sheetsql depend on `googleapis` lib in which you can set the following environment variables to proxy http/https requests: 99 | 100 | - `HTTP_PROXY` / `http_proxy` 101 | - `HTTPS_PROXY` / `https_proxy` 102 | 103 | The two environment variables could let your all requests using the proxy. If that is not your expected behavior and you only need to proxy google APIs, set `NO_PROXY=*`. 104 | 105 | Here is the [discuss](https://github.com/joway/sheetsql/issues/4). 106 | -------------------------------------------------------------------------------- /example.ts: -------------------------------------------------------------------------------- 1 | import * as bluebird from 'bluebird' 2 | import Database from './src' 3 | 4 | const DB = '1gSb-_vI8jk53UPaCXU9YGfeq2C9v2_k9tYC4ibdut98' 5 | 6 | const main = async () => { 7 | const db = new Database({ db: DB, table: 'Sheet1', keyFile: './google-serviceaccount.json' }) 8 | let count = 0 9 | while (true) { 10 | await db.insert([ 11 | { 12 | name: `user-${count}`, 13 | age: count, 14 | }, 15 | ]) 16 | if (count % 3 === 1) { 17 | await db.remove({ 18 | age: count - 1, 19 | }) 20 | } 21 | count++ 22 | 23 | console.log(`processed item ${count}`) 24 | await bluebird.delay(100) 25 | } 26 | } 27 | 28 | main() 29 | .then(() => process.exit(0)) 30 | .catch((err) => { 31 | console.error(err) 32 | process.exit(-1) 33 | }) 34 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { '^.+\\.ts?$': 'ts-jest' }, 3 | testEnvironment: 'node', 4 | testRegex: '(/tests/.*|(\\.|/)(test|spec))\\.tsx?$', 5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] 6 | } 7 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joway/sheetsql/f551ab82a96bea54155ac35305b0b21eda5b5582/logo.png -------------------------------------------------------------------------------- /logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joway/sheetsql/f551ab82a96bea54155ac35305b0b21eda5b5582/logo.sketch -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sheetsql", 3 | "version": "0.1.7", 4 | "description": "Google Spreadsheet as a Database", 5 | "main": "dist/src/index.js", 6 | "types": "dist/src/index.d.ts", 7 | "scripts": { 8 | "setup-test": "node ./gen-serviceaccount.js", 9 | "test": "npm run setup-test && npm run clean && jest", 10 | "clean": "rm -rf ./dist", 11 | "build": "npm run clean && tsc", 12 | "prepublish": "npm run build" 13 | }, 14 | "precommit": [ 15 | "prettier" 16 | ], 17 | "author": "Joway", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "@types/bluebird": "^3.5.26", 21 | "@types/jest": "^26.0.0", 22 | "@types/lodash": "^4.14.158", 23 | "@types/nedb": "^1.8.10", 24 | "jest": "^24.9.0", 25 | "ts-jest": "^26.1.4", 26 | "tslint-jike-node": "0.0.18", 27 | "typescript": "^3.3.4000" 28 | }, 29 | "dependencies": { 30 | "bluebird": "^3.5.3", 31 | "googleapis": "^55.0.0", 32 | "lodash": "^4.17.19" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joway/sheetsql/f551ab82a96bea54155ac35305b0b21eda5b5582/policy.png -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | import { Query, Document } from './types' 2 | import GoogleStorage, { IStorageOptions, IStorage } from './storage' 3 | 4 | export interface IDatabaseOptions extends IStorageOptions {} 5 | 6 | export default class Database { 7 | private storage: IStorage 8 | 9 | constructor(opts: IDatabaseOptions) { 10 | this.storage = new GoogleStorage(opts) 11 | } 12 | 13 | async load() { 14 | return this.storage.load() 15 | } 16 | 17 | async insert(docs: Document[]): Promise { 18 | return this.storage.insert(docs) 19 | } 20 | 21 | async find(query?: Query): Promise { 22 | return this.storage.find(query) 23 | } 24 | 25 | async findOne(query?: Query): Promise { 26 | const docs = await this.storage.find(query) 27 | return docs.length ? docs[0] : null 28 | } 29 | 30 | async update(query: Query, toUpdate: Document): Promise { 31 | return this.storage.update(query, toUpdate) 32 | } 33 | 34 | async updateOne(query: Query, toUpdate: Partial): Promise { 35 | const newDocs = await this.storage.update(query, toUpdate, { updatedOnce: true }) 36 | return newDocs.length ? newDocs[0] : null 37 | } 38 | 39 | async remove(query: Query): Promise { 40 | return this.storage.remove(query) 41 | } 42 | 43 | async removeOne(query: Query): Promise { 44 | const removedDocs = await this.storage.remove(query) 45 | return removedDocs.length ? removedDocs[0] : null 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | import { getErrors } from './utils/error' 2 | 3 | const errors = [ 4 | 'StorageOptionsError', 5 | 'StorageFormatError', 6 | 'StorageOptionError', 7 | ] 8 | 9 | export const Errors = getErrors(errors) 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Database from './db' 2 | 3 | export default Database 4 | export { Database } 5 | -------------------------------------------------------------------------------- /src/storage.ts: -------------------------------------------------------------------------------- 1 | import { google, sheets_v4 } from 'googleapis' 2 | import * as _ from 'lodash' 3 | import { Errors } from './errors' 4 | import { Query, Document } from './types' 5 | import * as bluebird from 'bluebird' 6 | 7 | export interface IStorage { 8 | insert(docs: Document[]): Promise 9 | find(query?: Query): Promise 10 | update( 11 | query: Query, 12 | toUpdate: Partial, 13 | opts?: { updatedOnce: boolean }, 14 | ): Promise 15 | remove(query: Query): Promise 16 | load(): Promise 17 | } 18 | 19 | export interface IStorageOptions { 20 | db: string // google spreadsheet id 21 | table?: string 22 | 23 | // google spreadsheet settings 24 | apiKey?: string 25 | keyFile?: string 26 | 27 | // storage settings 28 | cacheTimeoutMs?: number // ms 29 | } 30 | 31 | export default class GoogleStorage implements IStorage { 32 | private sheets: sheets_v4.Sheets 33 | private db: string 34 | private table: string 35 | private schema: string[] = [] 36 | private schemaMetaStore: { [name: string]: { col: number } } = {} 37 | private data: string[][] = new Array() 38 | private lastUpdated: Date | null = null 39 | private cacheTimeoutMs: number 40 | 41 | constructor(opts: IStorageOptions) { 42 | if (!opts.apiKey && !opts.keyFile) { 43 | throw new Errors.StorageOptionsError() 44 | } 45 | 46 | const auth = opts.apiKey 47 | ? opts.apiKey 48 | : new google.auth.GoogleAuth({ 49 | scopes: ['https://www.googleapis.com/auth/spreadsheets'], 50 | keyFile: opts.keyFile, 51 | }) 52 | this.sheets = google.sheets({ 53 | version: 'v4', 54 | auth: auth || '', 55 | }) 56 | this.db = opts.db 57 | this.table = opts.table || 'Sheet1' 58 | this.cacheTimeoutMs = opts.cacheTimeoutMs || 5000 59 | } 60 | 61 | _findRows(query?: Query): number[] { 62 | const queryKeys = _.keys(query) 63 | const nums: number[] = [] 64 | 65 | _.each(this.data, (row, rowNum) => { 66 | if (!query || !queryKeys.length) { 67 | nums.push(rowNum) 68 | return 69 | } 70 | 71 | for (const fieldName of queryKeys) { 72 | if (!this.schemaMetaStore[fieldName]) { 73 | return 74 | } 75 | const colNum = this.schemaMetaStore[fieldName].col 76 | const field = row[colNum] 77 | if (_.toString(field) !== _.toString(query[fieldName])) { 78 | return 79 | } 80 | } 81 | 82 | nums.push(rowNum) 83 | }) 84 | 85 | return nums 86 | } 87 | 88 | _rowToDoc = (row: string[]): Document => { 89 | const doc: Document = {} 90 | for (let colNum = 0; colNum < row.length; ++colNum) { 91 | const field = row[colNum] 92 | const fieldName = this.schema[colNum] 93 | doc[fieldName] = _.toString(field) 94 | } 95 | return doc 96 | } 97 | 98 | _docToRow = (doc: Document): string[] => { 99 | const row = new Array(this.schema.length) 100 | for (const fieldName of _.keys(doc)) { 101 | if (this.schemaMetaStore[fieldName]) { 102 | row[this.schemaMetaStore[fieldName].col] = _.toString(doc[fieldName]) 103 | } 104 | } 105 | return row 106 | } 107 | 108 | async find(query?: Query): Promise { 109 | await this._checkCacheTimeout() 110 | 111 | const rowNums = this._findRows(query) 112 | const docs = _.map(rowNums, (rowNum) => this._rowToDoc(this.data[rowNum])) 113 | return docs 114 | } 115 | 116 | async insert(docs: Document[]): Promise { 117 | await this._checkCacheTimeout() 118 | 119 | const rows = _.map(docs, this._docToRow) 120 | const range = this.table.includes('!') ? this.table : `${this.table}!A:A` 121 | await this.sheets.spreadsheets.values.append({ 122 | spreadsheetId: this.db, 123 | range, 124 | valueInputOption: 'USER_ENTERED', 125 | requestBody: { 126 | values: rows, 127 | }, 128 | }) 129 | this.data.push(...rows) 130 | 131 | return _.map(rows, this._rowToDoc) 132 | } 133 | 134 | async update( 135 | query: Query, 136 | toUpdate: Document, 137 | opts: { updatedOnce: boolean } = { updatedOnce: false }, 138 | ): Promise { 139 | await this._checkCacheTimeout() 140 | 141 | const rowNums = this._findRows(query) 142 | const { updatedOnce } = opts 143 | if (!rowNums.length) { 144 | return [] 145 | } 146 | 147 | const docs: Document[] = [] 148 | await bluebird.map( 149 | updatedOnce ? [rowNums[0]] : rowNums, 150 | async (rowNum) => { 151 | const oldDoc = this._rowToDoc(this.data[rowNum]) 152 | const newDoc = { ...oldDoc, ...toUpdate } 153 | const row = this._docToRow(newDoc) 154 | 155 | docs.push(this._rowToDoc(row)) 156 | 157 | await this.sheets.spreadsheets.values.update({ 158 | spreadsheetId: this.db, 159 | range: `${this.table}!A${rowNum + 2}`, 160 | valueInputOption: 'USER_ENTERED', 161 | requestBody: { 162 | values: [row], 163 | }, 164 | }) 165 | this.data[rowNum] = row 166 | }, 167 | { concurrency: 10 }, 168 | ) 169 | 170 | return docs 171 | } 172 | 173 | async remove(query: Query): Promise { 174 | await this._checkCacheTimeout() 175 | 176 | const rowNums = this._findRows(query) 177 | const emptyDoc = _.mapValues(this.schemaMetaStore, () => '') 178 | const emptyRow = this._docToRow(emptyDoc) 179 | const oldDoc: Document[] = [] 180 | await bluebird.map( 181 | rowNums, 182 | async (rowNum) => { 183 | oldDoc.push(this._rowToDoc(this.data[rowNum])) 184 | 185 | await this.sheets.spreadsheets.values.update({ 186 | spreadsheetId: this.db, 187 | range: `${this.table}!A${rowNum + 2}`, 188 | valueInputOption: 'USER_ENTERED', 189 | requestBody: { 190 | values: [emptyRow], 191 | }, 192 | }) 193 | this.data[rowNum] = emptyRow 194 | }, 195 | { concurrency: 10 }, 196 | ) 197 | 198 | return oldDoc 199 | } 200 | 201 | async _checkCacheTimeout(): Promise { 202 | if (!this.lastUpdated || Date.now() - this.lastUpdated.getTime() >= this.cacheTimeoutMs) { 203 | await this.load() 204 | return true 205 | } 206 | return false 207 | } 208 | 209 | async load() { 210 | const res = await this.sheets.spreadsheets.values.get({ 211 | range: this.table, 212 | spreadsheetId: this.db, 213 | }) 214 | const rows = res.data.values 215 | if (!rows || rows.length === 0) { 216 | throw new Errors.StorageFormatError() 217 | } 218 | 219 | const schema = rows[0] 220 | const schemaMetaStore: { [key: string]: { col: number } } = _.zipObject( 221 | rows[0], 222 | _.map(rows[0], (_, colNum) => ({ 223 | col: colNum, 224 | })), 225 | ) 226 | 227 | this.schema = schema 228 | this.schemaMetaStore = schemaMetaStore 229 | this.data = _.slice(rows, 1) 230 | this.lastUpdated = new Date() 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Document { 2 | [key: string]: any 3 | } 4 | export interface Query { 5 | [key: string]: any 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/error.ts: -------------------------------------------------------------------------------- 1 | export interface ErrorInput {} 2 | 3 | export function getErrors(errors: string[]) { 4 | const Err: { [key: string]: new () => Error } = {} 5 | for (const errName of errors) { 6 | Err[errName] = class extends Error { 7 | constructor() { 8 | super() 9 | this.name = errName 10 | } 11 | } 12 | } 13 | return Err 14 | } 15 | -------------------------------------------------------------------------------- /tests/main-test.ts: -------------------------------------------------------------------------------- 1 | import Database from '../src' 2 | 3 | // https://docs.google.com/spreadsheets/d/1ya2Tl2ev9M80xYwspv7FJaoWq0oVOMBk3VF0f0MXv2s/edit#gid=0 4 | const DB = '1ya2Tl2ev9M80xYwspv7FJaoWq0oVOMBk3VF0f0MXv2s' 5 | 6 | function getDB(table = 'Sheet1') { 7 | return new Database({ db: DB, table, keyFile: './google-serviceaccount.json' }) 8 | } 9 | 10 | beforeEach(async () => { 11 | const db = getDB() 12 | await db.load() 13 | 14 | await db.remove({}) 15 | }) 16 | 17 | afterAll(async () => { 18 | const db = getDB() 19 | await db.load() 20 | 21 | await db.remove({}) 22 | }) 23 | 24 | test('db simple', async () => { 25 | const db = getDB() 26 | await db.load() 27 | 28 | let docs = await db.insert([ 29 | { 30 | name: 'joway', 31 | age: 18, 32 | }, 33 | ]) 34 | expect(docs.length).toBe(1) 35 | expect(docs[0].name).toBe('joway') 36 | expect(docs[0].age).toBe('18') 37 | 38 | docs = await db.update( 39 | { 40 | name: 'joway', 41 | }, 42 | { 43 | age: 100, 44 | }, 45 | ) 46 | expect(docs[0].name).toBe('joway') 47 | expect(docs[0].age).toBe('100') 48 | 49 | docs = await db.find({ 50 | name: 'joway', 51 | }) 52 | expect(docs[0].name).toBe('joway') 53 | 54 | docs = await db.remove({ 55 | name: 'joway', 56 | }) 57 | expect(docs[0].name).toBe('joway') 58 | 59 | docs = await db.find({ 60 | name: 'joway', 61 | }) 62 | expect(docs.length).toBe(0) 63 | }, 30000) 64 | 65 | test('db find', async () => { 66 | const db = getDB() 67 | await db.load() 68 | 69 | let docs = await db.insert([ 70 | { 71 | name: 'jack', 72 | age: 18, 73 | }, 74 | { 75 | name: 'mark', 76 | age: 21, 77 | }, 78 | { 79 | name: 'jason', 80 | age: 18, 81 | no: 1, 82 | }, 83 | ]) 84 | expect(docs.length).toBe(3) 85 | 86 | docs = await db.find({ 87 | age: 18, 88 | }) 89 | expect(docs.length).toBe(2) 90 | 91 | docs = await db.find({ 92 | age: '18', 93 | }) 94 | expect(docs.length).toBe(2) 95 | 96 | docs = await db.find({}) 97 | expect(docs.length).toBe(3) 98 | 99 | docs = await db.find({ no: 1 }) 100 | expect(docs.length).toBe(0) 101 | 102 | docs = await db.find({ name: '' }) 103 | expect(docs.length).toBe(0) 104 | 105 | let doc = await db.updateOne({ name: 'jack' }, { name: '' }) 106 | expect(doc!.name).toBe('') 107 | 108 | docs = await db.find({ name: '' }) 109 | expect(docs.length).toBe(1) 110 | }, 30000) 111 | -------------------------------------------------------------------------------- /tests/public-test.ts: -------------------------------------------------------------------------------- 1 | import Database from '../src' 2 | 3 | const DB = '1J9CLF2GZroBpxMkjCoDXiXHL9o9kREv44q3Ia8Z6nGQ' 4 | 5 | function getDB() { 6 | return new Database({ db: DB, table: 'Sheet1', keyFile: './google-serviceaccount.json' }) 7 | } 8 | 9 | beforeEach(async () => { 10 | const db = getDB() 11 | await db.load() 12 | await db.remove({}) 13 | }) 14 | 15 | afterAll(async () => { 16 | const db = getDB() 17 | await db.load() 18 | await db.remove({}) 19 | }) 20 | 21 | test('db simple', async () => { 22 | const db = getDB() 23 | await db.load() 24 | 25 | let docs = await db.insert([ 26 | { 27 | name: 'joway', 28 | age: 18, 29 | }, 30 | ]) 31 | expect(docs.length).toBe(1) 32 | expect(docs[0].name).toBe('joway') 33 | expect(docs[0].age).toBe('18') 34 | 35 | docs = await db.update( 36 | { 37 | name: 'joway', 38 | }, 39 | { 40 | age: 100, 41 | }, 42 | ) 43 | expect(docs[0].name).toBe('joway') 44 | expect(docs[0].age).toBe('100') 45 | 46 | docs = await db.find({ 47 | name: 'joway', 48 | }) 49 | expect(docs[0].name).toBe('joway') 50 | 51 | docs = await db.remove({ 52 | name: 'joway', 53 | }) 54 | expect(docs[0].name).toBe('joway') 55 | 56 | docs = await db.find({ 57 | name: 'joway', 58 | }) 59 | expect(docs.length).toBe(0) 60 | }, 30000) 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "dist", 4 | "node_modules" 5 | ], 6 | "compileOnSave": true, 7 | "compilerOptions": { 8 | "outDir": "dist", 9 | "rootDir": "./", 10 | "module": "commonjs", 11 | "sourceMap": true, 12 | "declaration": true, 13 | "target": "es5", 14 | "strictNullChecks": true, 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "strict": true 19 | } 20 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-jike-node" 3 | } --------------------------------------------------------------------------------