UNPKG

@waline/vercel

Version:

vercel server for waline comment system

310 lines (250 loc) 7.67 kB
const { performance } = require('perf_hooks'); const { Deta } = require('deta'); const Base = require('./base.js'); module.exports = class extends Base { constructor(tableName) { super(tableName); const deta = Deta(process.env.DETA_PROJECT_KEY); this.instance = deta.Base(tableName); } complex(obj, keys) { const result = new Array(keys.reduce((a, b) => a * obj[b].length, 1)); for (let i = 0; i < result.length; i++) { result[i] = { ...obj }; for (let n = 0; n < keys.length; n++) { const divisor = keys .slice(n + 1) .reduce((a, b) => a * obj[b].length, 1); const idx = Math.floor(i / divisor) % obj[keys[n]].length; result[i][keys[n]] = obj[keys[n]][idx]; } } return result; } /** * deta base doesn't support order data by field * it will order by key default * so we need create a lower key than before to keep latest data in front * @returns string */ async uuid() { const items = await this.select({}, { limit: 1 }); let lastKey; if (items.length && !isNaN(parseInt(items[0].objectId))) { lastKey = parseInt(items[0].objectId); } else { lastKey = Number.MAX_SAFE_INTEGER - performance.now(); } return (lastKey - Math.round(Math.random() * 100)).toString(); } parseWhere(where) { if (think.isEmpty(where)) { return; } const parseKey = (k) => (k === 'objectId' ? 'key' : k); const conditions = {}; const _isArrayKeys = []; for (let k in where) { if (think.isString(where[k])) { conditions[parseKey(k)] = where[k]; continue; } if (where[k] === undefined) { conditions[parseKey(k)] = null; } if (!think.isArray(where[k]) || !where[k][0]) { continue; } const handler = where[k][0].toUpperCase(); switch (handler) { case 'IN': conditions[parseKey(k)] = where[k][1]; if (think.isArray(where[k][1])) { _isArrayKeys.push(parseKey(k)); } break; case 'NOT IN': /** * deta base doesn't support not equal with multiple value query * so we have to transfer it into equal with some value in most of scene */ if (Array.isArray(where[k][1]) && parseKey(k) === 'status') { const STATUS = ['approved', 'waiting', 'spam']; let val = STATUS.filter((s) => !where[k][1].includes(s)); if (val.length === 1) { val = val[0]; } conditions[parseKey(k)] = val; } conditions[parseKey(k) + '?ne'] = where[k][1]; break; case 'LIKE': { const first = where[k][1][0]; const last = where[k][1].slice(-1); if (first === '%' && last === '%') { conditions[parseKey(k) + '?contains'] = where[k][1].slice(1, -1); } else if (first === '%') { conditions[parseKey(k) + '?contains'] = where[k][1].slice(1); } else if (last === '%') { conditions[parseKey(k) + '?pfx'] = where[k][1].slice(0, -1); } break; } case '!=': conditions[parseKey(k) + '?ne'] = where[k][1]; break; case '>': conditions[parseKey(k) + '?gt'] = where[k][1]; break; } } if (_isArrayKeys.length === 0) { return conditions; } return this.complex(conditions, _isArrayKeys); } where(where) { const filter = this.parseWhere(where); if (!where._complex) { return filter; } const filters = []; for (const k in where._complex) { if (k === '_logic') { continue; } filters.push({ ...this.parseWhere({ [k]: where._complex[k] }), ...filter, }); } // just support OR logic for deta return filters; } async select(where, { limit, offset, field } = {}) { const conditions = this.where(where); if (think.isArray(conditions)) { return Promise.all( conditions.map((condition) => this.select(condition, { limit, offset, field }), ), ).then((data) => data.flat()); } let data = []; if ( think.isObject(conditions) && think.isString(conditions.key) && conditions.key ) { /** * deta base doesn't support fetch with key field query * if you want query by key field * you need use `get()` rather than `fetch()` method. */ const item = await this.instance.get(conditions.key); if (item) data.push(item); } else if (offset) { /** * deta base need last data key when pagination * so we need fetch data list again and again * because only that we can get last data key */ while (data.length < limit + offset) { const lastData = data[data.length - 1]; const last = lastData ? lastData.key : undefined; const { items } = await this.instance.fetch(conditions, { limit, last, }); data = data.concat(items); if (items.length < limit) { break; } } data = data.slice(offset, offset + limit); } else { const { items } = await this.instance.fetch(conditions, { limit: limit, }); data = items || []; } data = data.map(({ key, ...cmt }) => ({ ...cmt, objectId: key, })); if (Array.isArray(field)) { const fieldMap = new Set(field); fieldMap.add('objectId'); data.forEach((item) => { for (const k in item) { if (!fieldMap.has(k)) { delete item[k]; } } }); } return data; } async count(where = {}, { group } = {}) { if (!group) { const conditions = this.where(where); if (think.isArray(conditions)) { return Promise.all( conditions.map((condition) => this.count(condition)), ).then((counts) => counts.reduce((a, b) => a + b, 0)); } const { count } = await this.instance.fetch(conditions); return count; } const counts = []; for (let i = 0; i < group.length; i++) { const groupName = group[i]; if (!where._complex || !Array.isArray(where._complex[groupName])) { continue; } const groupFlatValue = {}; group.slice(0, i).forEach((group) => { groupFlatValue[group] = null; }); for (const item of where._complex[groupName][1]) { const groupWhere = { ...where, ...groupFlatValue, _complex: undefined, [groupName]: item, }; const num = await this.count(groupWhere); counts.push({ ...groupFlatValue, [groupName]: item, count: num, }); } } return counts; } async add(data) { const uuid = await this.uuid(); const resp = await this.instance.put(data, uuid); resp.objectId = resp.key; delete resp.key; return resp; } async update(data, where) { const items = await this.select(where); return Promise.all( items.map(async (item) => { const updateData = typeof data === 'function' ? data(item) : data; const nextData = { ...item, ...updateData }; await this.instance.put(nextData, item.objectId); return nextData; }), ); } async delete(where) { const items = await this.select(where); return Promise.all( items.map(({ objectId }) => this.instance.delete(objectId)), ); } };