spreadsheet-orm
Version:
ORM for Google Spreadsheet - Query Builder and Schema Management for spreadsheet database
579 lines (560 loc) • 18.4 kB
JavaScript
// 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
};