UNPKG

spreadsheet-orm

Version:

ORM for Google Spreadsheet - Query Builder and Schema Management for spreadsheet database

579 lines (560 loc) 18.4 kB
// src/client/SpreadsheetClient.ts var SpreadsheetClient = class { constructor(config, queryBuilder) { this.config = config; this.queryBuilder = queryBuilder; this.spreadsheetAPI = config.spread.API; this.spreadsheetID = config.spread.ID; } spreadsheetAPI; spreadsheetID; query(sql, values) { if (sql === void 0) { return this.queryBuilder; } return this.executeSql(sql, values); } async executeSql(sql, values) { } }; var SpreadsheetClient_default = SpreadsheetClient; // src/config/SchemaConfig.ts var SchemaConfig = class { definedSchema; missingSchemaStrategy; DEFAULT_MISSING_STRATEGY = "create"; constructor(options) { this.checkSchemaFormat(options); this.definedSchema = options.schemas; this.missingSchemaStrategy = options.onMissingSchema ?? this.DEFAULT_MISSING_STRATEGY; } checkSchemaFormat(options) { } }; var SchemaConfig_default = SchemaConfig; // src/config/SpreadConfig.ts import { GaxiosError } from "gaxios"; import { google } from "googleapis"; var SpreadConfig = class _SpreadConfig { static makeAuthJWT({ email, privateKey }) { return new google.auth.JWT({ email, key: privateKey, scopes: ["https://www.googleapis.com/auth/spreadsheets"] }); } static extractSheetIDfromURL(url) { const regex = /\/d\/([a-zA-Z0-9_-]{43})/; const match = url.match(regex); if (match && match[1]) { return match[1]; } return false; } /** * instance properties */ ID; authJWT; API; info = null; constructor(options) { this.checkFormat(options); this.ID = _SpreadConfig.extractSheetIDfromURL(options.spreadsheetID) || options.spreadsheetID; this.authJWT = _SpreadConfig.makeAuthJWT({ email: options.email, privateKey: options.privateKey }); this.API = google.sheets({ version: "v4", auth: this.authJWT }); } async getSpreadInfo({ cached } = { cached: false }) { if (cached && this.info) return this.info; const spreadsheetID = this.ID; try { this.API.spreadsheets.values; const response = await this.API.spreadsheets.get({ spreadsheetId: spreadsheetID }); this.info = response.data; } catch (error) { if (error instanceof GaxiosError) { const status = error.status; const message = error.response?.data.error.message; if (status === 404) { throw new Error(`cannot find spreadsheet with (ID:${spreadsheetID})`); } else if (status === 403) { throw new Error(`forbidden spreadsheet with (ID:${spreadsheetID})`); } else { throw new Error(`Error fetching spreadsheet: ${status} - ${message}`); } } } } checkFormat(options) { if (!this.isValidEmail(options.email)) { throw Error("Invalid email format"); } } isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } }; var SpreadConfig_default = SpreadConfig; // src/config/SheetConfig.ts var SheetConfig = class extends SpreadConfig_default { // lowest = 1 DEFAULT_RECORDING_START_ROW = 1; DEFAULT_COLUMN_NAME_SIZE = 1; DATA_STARTING_ROW = this.DEFAULT_RECORDING_START_ROW + this.DEFAULT_COLUMN_NAME_SIZE; // 따로 config 파일에서 사용하거나, default를 사용하거나 }; var SheetConfig_default = SheetConfig; // src/config/ClientConfig.ts var ClientConfig = class { spread; sheet; schema; constructor(opts) { this.spread = new SpreadConfig_default(opts); this.sheet = new SheetConfig_default(opts); this.schema = new SchemaConfig_default(opts); } // proxy // get projectId() { // return this.spreadsheet.projectId // } }; var ClientConfig_default = ClientConfig; // src/core/DDL/SchemaManager.ts var SchemaManager = class { constructor(options) { } }; var SchemaManager_default = SchemaManager; // src/types/mixin.ts function applyMixins(derivedCtor, baseCtors) { baseCtors.forEach((baseCtor) => { Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => { Object.defineProperty( derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || /* @__PURE__ */ Object.create(null) ); }); }); } var mixin_default = applyMixins; // src/core/DML/abstracts/BaseBuilder.ts var BaseBuilder = class { constructor(config) { this.config = config; } // protected sheetName?:string parseCell(cellAddress) { const match = cellAddress.match(/^([A-Z]+)(\d+)$/); if (!match) { throw new Error("Invalid A1 cell address format"); } const column = match[1]; const row = parseInt(match[2], 10); return { column, row }; } parseRange(range) { const [sheetNameOrCells, endPart] = range.split("!"); const hasSheetName = endPart !== void 0; const sheetName = hasSheetName ? sheetNameOrCells : void 0; const rangePart = hasSheetName ? endPart : sheetNameOrCells; const [startCell, endCell] = rangePart.split(":"); if (!startCell) { throw new Error("Invalid range format: startCell is missing"); } const start = this.parseCell(startCell); const end = endCell ? this.parseCell(endCell) : void 0; return { sheetName, startCell: start, endCell: end }; } composeRange(sheetName, row, specifiedColumn) { const startRow = typeof row === "number" ? row : row.startRow; const endRow = typeof row === "number" ? "" : row.endRow ?? startRow; const startColumn = (specifiedColumn && specifiedColumn.startColumn) ?? "A"; const endColumn = (specifiedColumn && specifiedColumn.endColumn) ?? "ZZZ"; return `${sheetName}!${startColumn}${startRow}:${endColumn}${endRow}`; } specifyColumn(columnNames) { const defaultColumns = { startColumn: null, endColumn: null }; return defaultColumns; if (!dummyDefinedColumn) return defaultColumns; const columnSpecification = columnNames.reduce((columnSpecification2, columnName) => { const targetColumn = dummyDefinedColumn[columnName]?.column; if (!targetColumn) return columnSpecification2; const { startColumn, endColumn } = columnSpecification2; if (!startColumn || !endColumn) { return { startColumn: targetColumn, endColumn: targetColumn }; } return { startColumn: targetColumn < startColumn ? targetColumn : startColumn, endColumn: targetColumn > endColumn ? targetColumn : endColumn }; }, defaultColumns); return columnSpecification; } extractValuesFromMatch(matchedValueRange) { const extractedValues = matchedValueRange.map((valueRangeObj) => { return valueRangeObj.valueRange?.values ?? []; }); return extractedValues; } }; var BaseBuilder_default = BaseBuilder; var dummyDefinedColumn = { "name": { column: "A" }, "class": { column: "B" }, "age": { column: "C" } }; // src/core/DML/abstracts/AndAble.ts var AndAble = class extends BaseBuilder_default { and(...ctorParam) { this.saveCurrentQueryToQueue(); console.log("queryQueue", this.queryQueue); const Constructor = this.constructor; const instance = new Constructor(this.config, ...ctorParam); instance["queryQueue"] = this.queryQueue; instance["sheetName"] = this.sheetName; return instance; } saveCurrentQueryToQueue() { this.queryQueue.push(this.createQueryForQueue()); } }; var AndAble_default = AndAble; // src/core/DML/abstracts/WhereAble.ts var WhereAble = class extends BaseBuilder_default { filterFN; where(param) { if (typeof param === "function") { this.filterFN = param; } return this; } // for one get ConditionedData async getConditionedData() { const range = this.composeRange(this.sheetName, this.config.sheet.DATA_STARTING_ROW); const dataFilters = this.makeDataFilters([range]); const batchData = await this.fetchBatchData(this.config.spread.ID, dataFilters); const batchValues = this.extractValuesFromMatch(batchData); const indexedBatchValue = this.indexingBatchData(batchValues[0]); const conditionedBatchValue = this.conditioning(indexedBatchValue); return conditionedBatchValue; } getCurrentCondition() { return { filterFN: this.filterFN }; } // 필터링, 인덱스를 포함하는 객체로 만든다 conditioning(dataWithRow, filterParam = { filterFN: this.filterFN }) { const { filterFN } = filterParam; if (filterFN) { return dataWithRow.filter((data) => filterFN(data)); } return dataWithRow; } async fetchBatchData(spreadsheetID, dataFilters) { const response = await this.config.spread.API.spreadsheets.values.batchGetByDataFilter({ spreadsheetId: spreadsheetID, requestBody: { dataFilters } }); const values = response.data.valueRanges; if (!values) throw Error("fail to fetch data"); return values; } indexingBatchData(data) { return data.map((currData, idx) => [idx + this.config.sheet.DATA_STARTING_ROW, ...currData]); } makeDataFilters(ranges) { return ranges.map((range) => ({ a1Range: range })); } }; var WhereAble_default = WhereAble; // src/core/DML/abstracts/mixins/ConditionChainQueryBuilder.ts var ConditionChainQueryBuilder = class extends BaseBuilder_default { chainConditioning(data) { return data.map((rangeData, idx) => { const filterParam = this.queryQueue[idx]; const indexedRangeData = this.indexingBatchData(rangeData); const result = this.conditioning(indexedRangeData, filterParam); return result; }); } async getChainConditionedData() { this.saveCurrentQueryToQueue(); const specifiedRanges = this.queryQueue.map((query) => this.composeRange(query.sheetName, this.config.sheet.DATA_STARTING_ROW)); const dataFilters = this.makeDataFilters(specifiedRanges); const batchDatas = await this.fetchBatchData(this.config.spread.ID, dataFilters); const batchValues = this.extractValuesFromMatch(batchDatas); const indexedBatchValues = batchValues.map((batchValue) => this.indexingBatchData(batchValue)); const conditionedBatchValues = indexedBatchValues.map((indexedBatchValue, idx) => this.conditioning(indexedBatchValue, this.queryQueue[idx])); return conditionedBatchValues; } }; mixin_default(ConditionChainQueryBuilder, [AndAble_default, WhereAble_default]); var ConditionChainQueryBuilder_default = ConditionChainQueryBuilder; // src/types/assertType.ts function assertNotNull(value) { if (value === null || value === void 0) { throw new Error("Value is null or undefined"); } } // src/core/DML/implements/SelectBuilder.ts var SelectBuilder = class _SelectBuilder extends ConditionChainQueryBuilder_default { // targetColumn 을 target으로 바꿔서, range or dml변수로 사용하도록 constructor(config, targetColumn = []) { super(config); this.targetColumn = targetColumn; } sheetName; // 필수 queryQueue = []; createQueryForQueue() { assertNotNull(this.sheetName); assertNotNull(this.targetColumn); console.log(this.queryQueue); const queryQueue = { ...this.getCurrentCondition(), sheetName: this.sheetName, targetColumn: this.targetColumn }; return queryQueue; } from(sheetName) { this.sheetName = sheetName; const instance = new _SelectBuilder(this.config, this.targetColumn); Object.assign(instance, this); return instance; } async execute() { this.saveCurrentQueryToQueue(); const compsedRanges = this.queryQueue.map((query) => { console.log(query.sheetName); const specifiedColumn = this.specifyColumn(query.targetColumn); const composedRange = this.composeRange(query.sheetName, this.config.sheet.DATA_STARTING_ROW, specifiedColumn); return composedRange; }); const requestBody = this.makeRequestBody(compsedRanges); console.log("requestBody", requestBody); const response = await this.config.spread.API.spreadsheets.values.batchGetByDataFilter({ spreadsheetId: this.config.spread.ID, requestBody }); const result = response.data.valueRanges; if (!result) throw Error("error"); const extractedValues = this.extractValuesFromMatch(result); const conditionedExtractedValues = this.chainConditioning(extractedValues); return conditionedExtractedValues; } makeRequestBody(ranges) { return { dataFilters: ranges.map((range) => ({ a1Range: range })) }; } }; var SelectBuilder_default = SelectBuilder; // src/core/DML/implements/UpdateBuilder.ts var UpdateBuilder = class _UpdateBuilder extends ConditionChainQueryBuilder_default { constructor(config, updateValues) { super(config); this.updateValues = updateValues; } sheetName; queryQueue = []; createQueryForQueue() { return { ...this.getCurrentCondition(), sheetName: this.sheetName, updateValues: this.updateValues }; } from(sheetName) { this.sheetName = sheetName; const instance = new _UpdateBuilder(this.config, this.updateValues); Object.assign(instance, this); return instance; } async execute() { const conditionedBatchValues = await this.getChainConditionedData(); const updateDataArr = conditionedBatchValues.map((conditionedBatchValue, idx) => { const { updateValues, sheetName } = this.queryQueue[idx]; const ranges = conditionedBatchValue.flatMap((data) => { const row = data.at(0); return this.composeRange(sheetName, { startRow: row, endRow: row }); }); return this.makeUpdateDataArr(ranges, updateValues); }).flat(); const response = await this.config.spread.API.spreadsheets.values.batchUpdateByDataFilter({ spreadsheetId: this.config.spread.ID, requestBody: { data: updateDataArr, valueInputOption: "RAW" } }); if (response.status !== 200) throw Error("error"); return response.data.totalUpdatedRows; } // and 를 위해 수정 필요 makeUpdateDataArr(ranges, values) { if (Array.isArray(values)) { return ranges.reduce((updateDataArr, range) => { updateDataArr.push({ dataFilter: { a1Range: range }, majorDimension: "ROWS", values: [values] }); return updateDataArr; }, []); } return []; } }; var UpdateBuilder_default = UpdateBuilder; // src/core/DML/implements/DeleteBuilder.ts var DeleteBuilder = class _DeleteBuilder extends ConditionChainQueryBuilder_default { sheetName; queryQueue = []; createQueryForQueue() { assertNotNull(this.sheetName); return { sheetName: this.sheetName, filterFN: this.filterFN }; } async execute() { const conditionedBatchValues = await this.getChainConditionedData(); const deleteDataArr = conditionedBatchValues.map((conditionedBatchValue, idx) => { const { sheetName } = this.queryQueue[idx]; const ranges = conditionedBatchValue.flatMap((data) => { const row = data.at(0); return this.composeRange(sheetName, { startRow: row, endRow: row }); }); return this.makeDeleteDataArr(ranges); }).flat(); const response = await this.config.spread.API.spreadsheets.values.batchClearByDataFilter({ spreadsheetId: this.config.spread.ID, requestBody: { dataFilters: deleteDataArr } }); if (response.status !== 200) throw Error("error"); return response.data.clearedRanges; } from(sheetName) { this.sheetName = sheetName; const instance = new _DeleteBuilder(this.config); Object.assign(instance, this); return instance; } // and 를 위해 수정 필요 makeDeleteDataArr(ranges) { return ranges.reduce((deleteDataArr, range) => { deleteDataArr.push({ a1Range: range }); return deleteDataArr; }, []); } constructor(config) { super(config); } }; var DeleteBuilder_default = DeleteBuilder; // src/core/DML/implements/InsertBuilder.ts var InsertBuilder = class _InsertBuilder extends AndAble_default { constructor(config, insertValues) { super(config); this.insertValues = insertValues; } sheetName; queryQueue = []; createQueryForQueue() { return { // Example structure, adjust as needed sheetName: this.sheetName, insertValues: this.insertValues }; } into(sheetName) { this.sheetName = sheetName; const instance = new _InsertBuilder(this.config, this.insertValues); Object.assign(instance, this); return instance; } async execute() { this.saveCurrentQueryToQueue(); const results = []; for (let i = 0; i < this.queryQueue.length; i++) { const response = await this.config.spread.API.spreadsheets.values.append({ spreadsheetId: this.config.spread.ID, valueInputOption: "RAW", range: this.queryQueue[i].sheetName, requestBody: { values: [this.queryQueue[i].insertValues] } }); if (response.status !== 200) throw Error("error"); results.push(response.data.updates?.updatedRows); } return results; } }; var InsertBuilder_default = InsertBuilder; // src/core/DML/QueryBuilder.ts var QueryBuilder = class { constructor(config) { this.config = config; } insert(insertValues) { return new InsertBuilder_default(this.config, insertValues); } select(targetColumn) { return new SelectBuilder_default(this.config, targetColumn); } update(updateValues) { return new UpdateBuilder_default(this.config, updateValues); } delete() { return new DeleteBuilder_default(this.config); } }; var QueryBuilder_default = QueryBuilder; // src/client/createSpreadsheetClient.ts function createSpreadsheetClient(opts) { const clientConfig = new ClientConfig_default(opts); const queryBuilder = new QueryBuilder_default(clientConfig); const schemaManager = new SchemaManager_default(clientConfig); const client = new SpreadsheetClient_default(clientConfig, queryBuilder); return client; } var createSpreadsheetClient_default = createSpreadsheetClient; // src/index.ts var index_default = createSpreadsheetClient_default; export { SpreadConfig_default as SpreadsheetConfig, index_default as default };