pebblebed
Version:
Simplified interactions with Google Datastore for NodeJS
272 lines (230 loc) • 9.32 kB
text/typescript
import DatastoreOperation from "./DatastoreOperation";
import PebblebedModel from "../PebblebedModel";
import Core from "../Core";
import { isNumber } from "../utility/BasicUtils";
import buildDataFromSchema from "../utility/buildDataFromSchema";
import extractSavedIds from "../utility/extractSavedIds";
import replaceIncompleteWithAllocatedIds from "../utility/replaceIncompleteWithAllocatedIds";
import { CreateMessage, throwError, warn } from "../Messaging";
import { EDebugPointId, IPebblebedSaveEntity } from "..";
import serializeJsonProperties from "../utility/serializeJsonProperties";
import { debugPoint } from "../debugging/DebugUtils";
import { convertSaveEntitiesToRegular } from "../utility/convertSaveEntitesToRegular";
import { convertDatastoreDataToRegularData } from "../utility/convertDatastoreDataToRegular";
import { Key, PathType } from "@google-cloud/datastore";
interface IOSaveRunResponse<T> {
generatedIds: (string | null)[],
savedEntities?: T[]
}
/*export interface IDatastoreSaveReturnEntities<T> extends DatastoreOperation<T> {
useTransaction(
transaction: any,
options?: {
allocateIdsNow?: boolean,
}
): IDatastoreSaveRegular<T>;
run(): Promise<{ generatedIds: (string|null)[], savedEntities?: T[] }>;
}
export interface IDatastoreSaveRegular<T> extends DatastoreOperation<T> {
useTransaction(
transaction: any,
options?: {
allocateIdsNow?: boolean,
}
): IDatastoreSaveRegular<T>;
returnSavedEntities(): IDatastoreSaveReturnEntities<T>;
run(): Promise<{ generatedIds: (string|null)[] }>;
}*/
export default class DatastoreSave<T, R extends IOSaveRunResponse<T> = { generatedIds: (string | null)[] }> extends DatastoreOperation<T> {
private dataObjects: any[];
private ignoreAnc = false;
private generate = false;
private transAllocateIds = false;
private returnSaved = false;
constructor(model: PebblebedModel<T>, data: T | T[]) {
super(model);
this.useCache = this.useCache ? Core.Instance.cacheDefaults.onSave : false;
if (Array.isArray(data)) {
this.dataObjects = data;
} else {
this.dataObjects = [data];
}
}
public useTransaction(
transaction: any,
options = {
allocateIdsNow: false,
}
) {
super.useTransaction(transaction);
this.transAllocateIds = options.allocateIdsNow;
return this;
}
public generateUnsetIds() {
this.generate = true;
return this;
}
public ignoreDetectedAncestors() {
this.ignoreAnc = true;
return this;
}
public returnSavedEntities(): DatastoreSave<T, { generatedIds: (string | null)[], savedEntities: T[] }> {
this.returnSaved = true;
return this as DatastoreSave<T, { generatedIds: (string | null)[], savedEntities: T[] }>;
}
public async run(): Promise<R> {
const baseKey = this.getBaseKey();
const cachingEnabled = this.useCache && Core.Instance.cacheStore != null && Core.Instance.cacheStore.cacheOnSave;
const cachableEntitySourceData: any[] = [];
const entities: IPebblebedSaveEntity<T>[] = this.dataObjects.map((data): IPebblebedSaveEntity<T> => {
let setAncestors = baseKey;
let id: PathType | null = null;
const entityKey: Key = data[Core.Instance.dsModule.KEY];
if (this.hasIdProperty && data[this.idProperty!] != null) {
switch (this.idType) {
case "string": {
if (typeof data[this.idProperty!] === "string") {
if (data[this.idProperty!].length === 0) {
throwError(CreateMessage.OPERATION_STRING_ID_EMPTY(this.model, "SAVE"));
}
id = data[this.idProperty!];
}
break;
}
case "int": {
if (isNumber(data[this.idProperty!])) {
id = Core.Instance.dsModule.int(data[this.idProperty!]);
}
break;
}
}
if (id == null) {
throwError(CreateMessage.OPERATION_DATA_ID_TYPE_ERROR(this.model, "SAVE", data[this.idProperty!]));
}
} else {
if (entityKey && entityKey.path && entityKey.path.length > 0 && entityKey.path.length % 2 === 0) {
if (entityKey.hasOwnProperty("id")) {
id = Core.Instance.dsModule.int(entityKey.id!);
} else {
id = entityKey.name!;
}
} else {
if (this.hasIdProperty && (this.idType === "string" || !this.generate)) {
throwError(CreateMessage.OPERATION_MISSING_ID_ERROR(this.model, "SAVE"));
}
}
}
if (!this.ignoreAnc && entityKey && entityKey.parent) {
if (setAncestors.length === 0) {
setAncestors = entityKey.parent.path;
} else {
const prevAncestors = entityKey.parent.path.toString();
const nextAncestors = setAncestors.toString();
if (prevAncestors !== nextAncestors) {
warn(
CreateMessage.OPERATION_CHANGED_ANCESTORS_WARNING(
this.model,
"SAVE",
prevAncestors,
nextAncestors
)
);
}
}
}
if (this.runValidation && this.model.entityPebbleSchema != null) {
// const validation = Core.Joi.validate(data, this.model.entityPebbleSchema.__getJoiSchema());
const validation = this.model.entityPebbleSchema.__getJoiSchema().validate(data);
if (validation.error != null) {
throwError(`On save entity of kind -> ${this.model.entityKind} : ${validation.error.message}`);
}
}
const key: Key = id
? this.createFullKey(setAncestors.concat([this.kind, id]), entityKey)
: this.createFullKey(setAncestors.concat([this.kind]), entityKey);
const generated = (id == null);
if (entityKey) {
delete data[Core.Instance.dsModule.KEY];
}
const {dataObject, excludeFromIndexes} = buildDataFromSchema(data, this.schema, this.kind);
if (cachingEnabled) {
cachableEntitySourceData.push({
key,
data: convertDatastoreDataToRegularData(dataObject, this.schema),
generated
})
}
return {
key,
excludeFromIndexes,
generated,
data: dataObject as T,
};
});
debugPoint(EDebugPointId.ON_SAVE_BUILT_ENTITIES, entities);
if (this.transaction) {
if (this.transAllocateIds) {
const {newEntities, ids} = await replaceIncompleteWithAllocatedIds<T>(entities, this.transaction);
this.transaction.save(newEntities);
return {
generatedIds: ids,
...this.returnSaved && {
savedEntities: convertSaveEntitiesToRegular(newEntities, this.idProperty, this.idType, this.schema),
},
} as R;
}
this.transaction.save(entities);
return {
get generatedIds() {
warn(CreateMessage.ACCESS_TRANSACTION_GENERATED_IDS_ERROR);
return [null];
},
...this.returnSaved && {
savedEntities: convertSaveEntitiesToRegular(entities, this.idProperty, this.idType, this.schema),
},
} as R;
}
return Core.Instance.dsModule.save(entities).then((data): R => {
const saveResponse = extractSavedIds(data)[0];
if (cachingEnabled && cachableEntitySourceData.length > 0) {
const cacheEntities: any[] = [];
for (let i = 0; i < cachableEntitySourceData.length; i += 1) {
cacheEntities.push({
[Core.Instance.dsModule.KEY]: cachableEntitySourceData[i].key,
...cachableEntitySourceData[i].data,
});
// Get the generated IDs from the save response (it returns the generated IDs on save)
if (cachableEntitySourceData[i].generated) {
cacheEntities[i][Core.Instance.dsModule.KEY].path.push(saveResponse.generatedIds[i]);
if (this.idType === "int") {
cacheEntities[i][Core.Instance.dsModule.KEY].id = saveResponse.generatedIds[i];
} else {
cacheEntities[i][Core.Instance.dsModule.KEY].name = saveResponse.generatedIds[i];
}
}
}
if (cacheEntities.length > 0) {
serializeJsonProperties(cacheEntities, this.schema);
Core.Instance.cacheStore!.setEntitiesAfterLoadOrSave(cacheEntities, this.cachingTimeSeconds);
}
}
if (this.returnSaved) {
return {
generatedIds: saveResponse.generatedIds,
savedEntities: convertSaveEntitiesToRegular(entities.map((e, i) => {
if (e.generated) {
e.key.path.push(saveResponse.generatedIds[i]);
if (this.idType === "int") {
e.key.id = saveResponse.generatedIds[i];
} else {
e.key.name = saveResponse.generatedIds[i];
}
}
return e;
}), this.idProperty, this.idType, this.schema),
} as R
}
return saveResponse as R;
});
}
}