import { CouchDBAdapter, CouchDBKeys, MangoQuery } from "@decaf-ts/for-couchdb";
import { Model, ValidationKeys } from "@decaf-ts/decorator-validation";
import { FabricContractFlags } from "./types";
import { FabricContractContext } from "./ContractContext";
import {
BadRequestError,
BaseError,
ConflictError,
InternalError,
NotFoundError,
onCreate,
onCreateUpdate,
PrimaryKeyType,
SerializationError,
} from "@decaf-ts/db-decorators";
import {
Context as Ctx,
Object as FabricObject,
Property,
Property as FabricProperty,
} from "fabric-contract-api";
import { Logger, Logging } from "@decaf-ts/logging";
import {
PersistenceKeys,
RelationsMetadata,
Sequence,
SequenceOptions,
UnsupportedError,
Adapter,
PreparedModel,
Repository,
QueryError,
PagingError,
MigrationError,
ObserverError,
AuthorizationError,
ForbiddenError,
ConnectionError,
ContextualizedArgs,
Context,
RawResult,
Paginator,
ContextualArgs,
MaybeContextualArg,
MethodOrOperation,
AllOperationKeys,
FlagsOf,
ContextOf,
} from "@decaf-ts/core";
import { FabricContractRepository } from "./FabricContractRepository";
import {
ChaincodeStub,
ClientIdentity,
Iterators,
StateQueryResponse,
} from "fabric-shim-api";
import { FabricStatement } from "./FabricContractStatement";
import { FabricContractSequence } from "./FabricContractSequence";
import { FabricFlavour } from "../shared/constants";
import { SimpleDeterministicSerializer } from "../shared/SimpleDeterministicSerializer";
import {
Constructor,
Decoration,
Metadata,
propMetadata,
} from "@decaf-ts/decoration";
import { FabricContractPaginator } from "./FabricContractPaginator";
import { MissingContextError } from "../shared/errors";
export type FabricContextualizedArgs<
ARGS extends any[] = any[],
EXTEND extends boolean = false,
> = ContextualizedArgs<FabricContractContext, ARGS, EXTEND> & {
stub: ChaincodeStub;
identity: ClientIdentity;
};
/**
* @description Sets the creator or updater field in a model based on the user in the context
* @summary Callback function used in decorators to automatically set the created_by or updated_by fields
* with the username from the context when a document is created or updated
* @template M - Type extending Model
* @template R - Type extending NanoRepository<M>
* @template V - Type extending RelationsMetadata
* @param {R} this - The repository instance
* @param {FabricContractContext} context - The operation context containing user information
* @param {V} data - The relation metadata
* @param {string} key - The property key to set with the username
* @param {M} model - The model instance being created or updated
* @return {Promise<void>} A promise that resolves when the operation is complete
* @function createdByOnFabricCreateUpdate
* @memberOf module:fabric.contracts
* @mermaid
* sequenceDiagram
* participant F as createdByOnNanoCreateUpdate
* participant C as Context
* participant M as Model
* F->>C: get("user")
* C-->>F: user object
* F->>M: set key to user.name
* Note over F: If no user in context
* F-->>F: throw UnsupportedError
*/
export async function createdByOnFabricCreateUpdate<
M extends Model,
R extends FabricContractRepository<M>,
V extends RelationsMetadata,
>(
this: R,
context: ContextOf<R>,
data: V,
key: keyof M,
model: M
): Promise<void> {
try {
const user = context.get("identity") as ClientIdentity;
model[key] = user.getID() as M[typeof key];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e: unknown) {
throw new UnsupportedError(
"No User found in context. Please provide a user in the context"
);
}
}
/**
* @description Adapter for Hyperledger Fabric chaincode state database operations
* @summary Provides a CouchDB-like interface for interacting with the Fabric state database from within a chaincode contract
* @template void - No configuration needed for contract adapter
* @template FabricContractFlags - Flags specific to Fabric contract operations
* @template FabricContractContext - Context type for Fabric contract operations
* @class FabricContractAdapter
* @example
* ```typescript
* // In a Fabric chaincode contract class
* import { FabricContractAdapter } from '@decaf-ts/for-fabric';
*
* export class MyContract extends Contract {
* private adapter = new FabricContractAdapter();
*
* @Transaction()
* async createAsset(ctx: Context, id: string, data: string): Promise<void> {
* const model = { id, data, timestamp: Date.now() };
* await this.adapter.create('assets', id, model, {}, { stub: ctx.stub });
* }
* }
* ```
* @mermaid
* sequenceDiagram
* participant Contract
* participant FabricContractAdapter
* participant Stub
* participant StateDB
*
* Contract->>FabricContractAdapter: create(tableName, id, model, transient, ctx)
* FabricContractAdapter->>FabricContractAdapter: Serialize model to JSON
* FabricContractAdapter->>Stub: putState(id, serializedData)
* Stub->>StateDB: Write data
* StateDB-->>Stub: Success
* Stub-->>FabricContractAdapter: Success
* FabricContractAdapter-->>Contract: model
*/
export class FabricContractAdapter extends CouchDBAdapter<
any,
void,
FabricContractContext
> {
protected override getClient(): void {
throw new UnsupportedError("Client is not supported in Fabric contracts");
}
/**
* @description Text decoder for converting binary data to strings
*/
private static textDecoder = new TextDecoder("utf8");
protected static readonly serializer = new SimpleDeterministicSerializer();
/**
* @description Context constructor for this adapter
* @summary Overrides the base Context constructor with FabricContractContext
*/
protected override get Context(): Constructor<FabricContractContext> {
return FabricContractContext;
}
/**
* @description Gets the repository constructor for this adapter
* @summary Returns the FabricContractRepository constructor for creating repositories
* @template M - Type extending Model
* @return {Constructor<Repository<M, MangoQuery, FabricContractAdapter, FabricContractFlags, FabricContractContext>>} The repository constructor
*/
override repository<
R extends Repository<
any,
Adapter<any, void, MangoQuery, Context<FabricContractFlags>>
>,
>(): Constructor<R> {
return FabricContractRepository as unknown as Constructor<R>;
}
override Paginator<M extends Model>(
query: MangoQuery,
size: number,
clazz: Constructor<M>
): Paginator<M, any, MangoQuery> {
return new FabricContractPaginator(this, query, size, clazz);
}
override async Sequence(options: SequenceOptions): Promise<Sequence> {
return new FabricContractSequence(options, this as any);
}
/**
* @description Creates a new FabricContractAdapter instance
* @summary Initializes an adapter for interacting with the Fabric state database
* @param {void} scope - Not used in this adapter
* @param {string} [alias] - Optional alias for the adapter instance
*/
constructor(scope: void, alias?: string) {
super(scope, FabricFlavour, alias);
}
override for(config: Partial<any>, ...args: any): typeof this {
return super.for(config, ...args);
}
/**
* @description Creates a record in the state database
* @summary Serializes a model and stores it in the Fabric state database
* @param {string} tableName - The name of the table/collection
* @param {string | number} id - The record identifier
* @param {Record<string, any>} model - The record data
* @param {Record<string, any>} transient - Transient data (not used in this implementation)
* @param {...any[]} args - Additional arguments, including the chaincode stub and logger
* @return {Promise<Record<string, any>>} Promise resolving to the created record
*/
override async create<M extends Model>(
clazz: Constructor<M>,
id: PrimaryKeyType,
model: Record<string, any>,
...args: ContextualArgs<Context<FabricContractFlags>>
): Promise<Record<string, any>> {
const { ctx, log } = this.logCtx(args, this.create);
log.info(`in ADAPTER create with args ${args}`);
const tableName = Model.tableName(clazz);
try {
log.info(`adding entry to ${tableName} table with pk ${id}`);
const composedKey = ctx.stub.createCompositeKey(tableName, [String(id)]);
model = await this.putState(composedKey, model, ctx);
} catch (e: unknown) {
throw this.parseError(e as Error);
}
return model;
}
/**
* @description Reads a record from the state database
* @summary Retrieves and deserializes a record from the Fabric state database
* @param {string} tableName - The name of the table/collection
* @param {string | number} id - The record identifier
* @param {...any[]} args - Additional arguments, including the chaincode stub and logger
* @return {Promise<Record<string, any>>} Promise resolving to the retrieved record
*/
override async read<M extends Model>(
clazz: Constructor<M>,
id: PrimaryKeyType,
...args: ContextualArgs<Context<FabricContractFlags>>
): Promise<Record<string, any>> {
const { ctx, log } = this.logCtx(args, this.read);
log.info(`in ADAPTER read with args ${args}`);
const tableName = Model.tableName(clazz);
let model: Record<string, any>;
try {
const composedKey = ctx.stub.createCompositeKey(tableName, [String(id)]);
model = await this.readState(composedKey, ctx);
} catch (e: unknown) {
throw this.parseError(e as Error);
}
return model;
}
/**
* @description Updates a record in the state database
* @summary Serializes a model and updates it in the Fabric state database
* @param {string} tableName - The name of the table/collection
* @param {string | number} id - The record identifier
* @param {Record<string, any>} model - The updated record data
* @param {Record<string, any>} transient - Transient data (not used in this implementation)
* @param {...any[]} args - Additional arguments, including the chaincode stub and logger
* @return {Promise<Record<string, any>>} Promise resolving to the updated record
*/
override async update<M extends Model>(
clazz: Constructor<M>,
id: PrimaryKeyType,
model: Record<string, any>,
...args: ContextualArgs<Context<FabricContractFlags>>
): Promise<Record<string, any>> {
const { ctx, log } = this.logCtx(args, this.update);
const tableName = Model.tableName(clazz);
try {
log.verbose(`updating entry to ${tableName} table with pk ${id}`);
const composedKey = ctx.stub.createCompositeKey(tableName, [String(id)]);
model = await this.putState(composedKey, model, ctx);
} catch (e: unknown) {
throw this.parseError(e as Error);
}
return model;
}
/**
* @description Deletes a record from the state database
* @summary Retrieves a record and then removes it from the Fabric state database
* @param {string} tableName - The name of the table/collection
* @param {string | number} id - The record identifier to delete
* @param {...any[]} args - Additional arguments, including the chaincode stub and logger
* @return {Promise<Record<string, any>>} Promise resolving to the deleted record
*/
async delete<M extends Model>(
clazz: Constructor<M>,
id: PrimaryKeyType,
...args: ContextualArgs<Context<FabricContractFlags>>
): Promise<Record<string, any>> {
const { ctx, log, ctxArgs } = this.logCtx(args, this.delete);
const tableName = Model.tableName(clazz);
let model: Record<string, any>;
try {
const composedKey = ctx.stub.createCompositeKey(tableName, [String(id)]);
model = await this.read(clazz, id, ...ctxArgs);
log.verbose(`deleting entry with pk ${id} from ${tableName} table`);
await this.deleteState(composedKey, ctx);
} catch (e: unknown) {
throw this.parseError(e as Error);
}
return model;
}
protected async deleteState(id: string, context: FabricContractContext) {
const { ctx } = this.logCtx([context], this.deleteState);
await ctx.stub.deleteState(id);
}
forPrivate(collection: string): FabricContractAdapter {
const toOverride = [
this.putState,
this.readState,
this.deleteState,
this.queryResult,
this.queryResultPaginated,
].map((fn) => fn.name);
return new Proxy(this, {
get(target, prop, receiver) {
if (!toOverride.includes(prop as string))
return Reflect.get(target, prop, receiver);
return new Proxy((target as any)[prop], {
async apply(fn, thisArg, argsList) {
switch (prop) {
case "putState": {
const [stub, id, model] = argsList;
await stub.putPrivateData(collection, id.toString(), model);
return model;
}
case "deleteState": {
const [stub, id] = argsList;
return (stub as ChaincodeStub).deletePrivateData(
collection,
id
);
}
case "readState": {
const [stub, id] = argsList;
return stub.getPrivateData(collection, id);
}
case "queryResult": {
const [stub, rawInput] = argsList;
return stub.getPrivateDataQueryResult(collection, rawInput);
}
case "queryResultPaginated": {
const [stub, rawInput, limit, skip] = argsList;
const iterator = await (
stub as ChaincodeStub
).getPrivateDataQueryResult(collection, rawInput);
const results: any[] = [];
let count = 0;
let reachedBookmark = skip ? false : true;
let lastKey: string | null = null;
while (true) {
const res = await iterator.next();
if (res.value && res.value.value.toString()) {
const recordKey = res.value.key;
const recordValue = (res.value.value as any).toString(
"utf8"
);
// If we have a skip, skip until we reach it
if (!reachedBookmark) {
if (recordKey === skip?.toString()) {
reachedBookmark = true;
}
continue;
}
results.push({
Key: recordKey,
Record: JSON.parse(recordValue),
});
lastKey = recordKey;
count++;
if (count >= limit) {
await iterator.close();
return {
iterator:
results as unknown as Iterators.StateQueryIterator,
metadata: {
fetchedRecordsCount: results.length,
bookmark: lastKey,
},
};
}
}
if (res.done) {
await iterator.close();
return {
iterator:
results as unknown as Iterators.StateQueryIterator,
metadata: {
fetchedRecordsCount: results.length,
bookmark: "",
},
};
}
}
}
default:
throw new InternalError(
`Unsupported method override ${String(prop)}`
);
}
},
});
},
});
}
protected async putState(
id: string,
model: Record<string, any>,
ctx: FabricContractContext
) {
let data: Buffer;
const { log } = this.logCtx([ctx], this.putState);
try {
data = Buffer.from(
FabricContractAdapter.serializer.serialize(model as Model, false)
);
} catch (e: unknown) {
throw new SerializationError(
`Failed to serialize record with id ${id}: ${e}`
);
}
const collection = ctx.get("segregated");
if (collection)
await ctx.stub.putPrivateData(collection, id.toString(), data);
else await ctx.stub.putState(id.toString(), data);
log.silly(
`state stored${collection ? ` in ${collection} collection` : ""} under id ${id}`
);
return model;
}
protected async readState(id: string, ctx: FabricContractContext) {
let result: any;
const { log } = this.logCtx([ctx], this.readState);
let res: string;
const collection = ctx.get("segregated");
if (collection)
res = (
await ctx.stub.getPrivateData(collection, id.toString())
).toString();
else res = (await ctx.stub.getState(id.toString())).toString();
if (!res)
throw new NotFoundError(
`Record with id ${id}${collection ? ` in ${collection} collection` : ""} not found`
);
log.silly(
`state retrieved from${collection ? ` ${collection} collection` : ""} under id ${id}`
);
try {
result = FabricContractAdapter.serializer.deserialize(res.toString());
} catch (e: unknown) {
throw new SerializationError(`Failed to parse record: ${e}`);
}
return result;
}
protected async queryResult(
stub: ChaincodeStub,
rawInput: any,
...args: ContextualArgs<FabricContractContext>
): Promise<Iterators.StateQueryIterator> {
const { ctx } = this.logCtx(args, this.queryResult);
let res: Iterators.StateQueryIterator;
const collection = ctx.get("segregated");
if (collection)
res = await ctx.stub.getPrivateDataQueryResult(
collection,
JSON.stringify(rawInput)
);
else res = await stub.getQueryResult(JSON.stringify(rawInput));
return res;
}
protected async queryResultPaginated(
stub: ChaincodeStub,
rawInput: any,
limit: number = 250,
page?: number,
bookmark?: string | number,
...args: any[]
): Promise<StateQueryResponse<Iterators.StateQueryIterator>> {
const { ctx } = this.logCtx(args, this.readState);
let res: StateQueryResponse<Iterators.StateQueryIterator>;
const collection = ctx.get("segregated");
if (collection) {
if (bookmark) rawInput.selector._id = { $gt: bookmark.toString() };
const it = await stub.getPrivateDataQueryResult(
collection,
JSON.stringify(rawInput)
);
res = {
iterator: it,
metadata: {
fetchedRecordsCount: limit,
bookmark: "",
},
};
} else
res = await stub.getQueryResultWithPagination(
JSON.stringify(rawInput),
limit,
bookmark?.toString()
);
return res;
}
protected mergeModels(results: Record<string, any>[]): Record<string, any> {
const extract = (model: Record<string, any>) =>
Object.entries(model).reduce((accum: Record<string, any>, [key, val]) => {
if (typeof val !== "undefined") accum[key] = val;
return accum;
}, {});
let finalModel: Record<string, any> = results.pop() as Record<string, any>;
for (const res of results) {
finalModel = Object.assign({}, extract(finalModel), extract(res));
}
return finalModel;
}
/**
* @description Decodes binary data to string
* @summary Converts a Uint8Array to a string using UTF-8 encoding
* @param {Uint8Array} buffer - The binary data to decode
* @return {string} The decoded string
*/
protected decode(buffer: Uint8Array) {
return FabricContractAdapter.textDecoder.decode(buffer);
}
/**
* @description Creates operation flags for Fabric contract operations
* @summary Merges default flags with Fabric-specific context information
* @template M - Type extending Model
* @param {OperationKeys} operation - The operation being performed
* @param {Constructor<M>} model - The model constructor
* @param {Partial<FabricContractFlags>} flags - Partial flags to merge with defaults
* @param {Ctx} ctx - The Fabric chaincode context
* @return {FabricContractFlags} The merged flags
*/
protected override async flags<M extends Model>(
operation: AllOperationKeys,
model: Constructor<M> | undefined,
flags: Partial<FabricContractFlags> | FabricContractContext | Ctx | any
): Promise<FabricContractFlags> {
let baseFlags = Object.assign(
{
segregated: false,
},
flags
);
if (flags instanceof FabricContractContext) {
// do nothing
} else if ((flags as Ctx).stub) {
Object.assign(baseFlags, {
stub: flags.stub,
identity: (flags as Ctx).clientIdentity,
cert: (flags as Ctx).clientIdentity.getIDBytes().toString(),
roles: (flags as Ctx).clientIdentity.getAttributeValue("roles"),
logger: Logging.for(
operation,
{
logLevel: false,
timestamp: false,
correlationId: (flags as Ctx).stub.getTxID(),
},
flags
),
correlationId: (flags as Ctx).stub.getTxID(),
});
} else {
baseFlags = Object.assign(baseFlags, flags || {});
}
return (await super.flags(
operation,
model,
baseFlags as any
)) as FabricContractFlags;
}
/**
* @description Creates an index for a model
* @summary This method is not implemented for Fabric contracts and returns a resolved promise
* @template M - Type extending Model
* @param {Constructor<M>} models - The model constructor
* @return {Promise<void>} Promise that resolves immediately
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected index<M>(models: Constructor<M>): Promise<void> {
return Promise.resolve(undefined);
}
/**
* @description Processes results from a state query iterator
* @summary Iterates through query results and converts them to a structured format
* @param {Logger} log - Logger instance for debugging
* @param {Iterators.StateQueryIterator} iterator - The state query iterator
* @param {boolean} [isHistory=false] - Whether this is a history query
* @return {Promise<any[]>} Promise resolving to an array of processed results
* @mermaid
* sequenceDiagram
* participant Caller
* participant ResultIterator
* participant Iterator
*
* Caller->>ResultIterator: resultIterator(log, iterator, isHistory)
* loop Until done
* ResultIterator->>Iterator: next()
* Iterator-->>ResultIterator: { value, done }
* alt Has value
* ResultIterator->>ResultIterator: Process value based on isHistory
* ResultIterator->>ResultIterator: Add to results array
* end
* end
* ResultIterator->>Iterator: close()
* ResultIterator-->>Caller: allResults
*/
protected async resultIterator(
log: Logger,
iterator: Iterators.StateQueryIterator,
isHistory = false
) {
const allResults = [];
let res: { value: any; done: boolean } = await iterator.next();
while (!res.done) {
if (res.value && res.value.value.toString()) {
let jsonRes: any = {};
log.debug(res.value.value.toString("utf8"));
if (isHistory /* && isHistory === true*/) {
jsonRes.TxId = res.value.txId;
jsonRes.Timestamp = res.value.timestamp;
try {
jsonRes.Value = JSON.parse(res.value.value.toString("utf8"));
} catch (err: any) {
log.error(err);
jsonRes.Value = res.value.value.toString("utf8");
}
} else {
try {
jsonRes = JSON.parse(res.value.value.toString("utf8"));
} catch (err: any) {
log.error(err);
jsonRes = res.value.value.toString("utf8");
}
}
allResults.push(jsonRes);
}
res = await iterator.next();
}
log.debug(`Closing iterator after ${allResults.length} results`);
iterator.close(); // purposely not await. let iterator close on its own
return allResults;
}
/**
* @description Executes a raw query against the state database
* @summary Performs a rich query using CouchDB syntax against the Fabric state database
* @template R - The return type
* @param {MangoQuery} rawInput - The Mango Query to execute
* @param {boolean} docsOnly - Whether to return only documents (not used in this implementation)
* @param {...any[]} args - Additional arguments, including the chaincode stub and logger
* @return {Promise<R>} Promise resolving to the query results
* @mermaid
* sequenceDiagram
* participant Caller
* participant FabricContractAdapter
* participant Stub
* participant StateDB
*
* Caller->>FabricContractAdapter: raw(rawInput, docsOnly, ctx)
* FabricContractAdapter->>FabricContractAdapter: Extract limit and skip
* alt With pagination
* FabricContractAdapter->>Stub: getQueryResultWithPagination(query, limit, skip)
* else Without pagination
* FabricContractAdapter->>Stub: getQueryResult(query)
* end
* Stub->>StateDB: Execute query
* StateDB-->>Stub: Iterator
* Stub-->>FabricContractAdapter: Iterator
* FabricContractAdapter->>FabricContractAdapter: resultIterator(log, iterator)
* FabricContractAdapter-->>Caller: results
*/
async raw<R, D extends boolean>(
rawInput: MangoQuery,
docsOnly: D = true as D,
...args: ContextualArgs<FabricContractContext>
): Promise<RawResult<R, D>> {
const { log, ctx } = this.logCtx(args, this.raw);
const { skip, limit } = rawInput;
let iterator: Iterators.StateQueryIterator;
const resp = { docs: [], bookmark: undefined as string | undefined };
if (limit || skip) {
delete rawInput["limit"];
delete rawInput["skip"];
log.debug(
`Retrieving paginated iterator: limit: ${limit}/ skip: ${skip}`
);
const response: StateQueryResponse<Iterators.StateQueryIterator> =
(await this.queryResultPaginated(
ctx.stub,
rawInput,
limit || Number.MAX_VALUE,
(skip as any)?.toString(),
rawInput["bookmark"],
ctx
)) as StateQueryResponse<Iterators.StateQueryIterator>;
resp.bookmark = response.metadata.bookmark;
iterator = response.iterator;
} else {
log.debug("Retrieving iterator");
iterator = (await this.queryResult(
ctx.stub,
rawInput,
ctx
)) as Iterators.StateQueryIterator;
}
log.debug("Iterator acquired");
resp.docs = (await this.resultIterator(log, iterator)) as any;
log.debug(
`returning ${Array.isArray(resp.docs) ? resp.docs.length : 1} results`
);
if (docsOnly) return resp.docs as any;
return resp as any;
}
override Statement<M extends Model>(): FabricStatement<M, any> {
return new FabricStatement(this as any);
}
override async createAll<M extends Model>(
tableName: Constructor<M>,
id: PrimaryKeyType[],
model: Record<string, any>[],
...args: ContextualArgs<FabricContractContext>
): Promise<Record<string, any>[]> {
if (id.length !== model.length)
throw new InternalError("Ids and models must have the same length");
const { log, ctxArgs } = this.logCtx(args, this.createAll);
const tableLabel = Model.tableName(tableName);
log.debug(`Creating ${id.length} entries ${tableLabel} table`);
return Promise.all(
id.map((i, count) => this.create(tableName, i, model[count], ...ctxArgs))
);
}
override async updateAll<M extends Model>(
tableName: Constructor<M>,
id: PrimaryKeyType[],
model: Record<string, any>[],
...args: ContextualArgs<FabricContractContext>
): Promise<Record<string, any>[]> {
if (id.length !== model.length)
throw new InternalError("Ids and models must have the same length");
const { log, ctxArgs } = this.logCtx(args, this.updateAll);
const tableLabel = Model.tableName(tableName);
log.debug(`Updating ${id.length} entries ${tableLabel} table`);
return Promise.all(
id.map((i, count) => this.update(tableName, i, model[count], ...ctxArgs))
);
}
/**
*
* @param model
* @param {string} pk
* @param args
*/
override prepare<M extends Model>(
model: M,
...args: ContextualArgs<FabricContractContext>
): PreparedModel {
const { log } = this.logCtx(args, this.prepare);
const tableName = Model.tableName(model.constructor as any);
const pk = Model.pk(model.constructor as any);
const split = Model.segregate(model);
const result = Object.entries(split.model).reduce(
(accum: Record<string, any>, [key, val]) => {
if (typeof val === "undefined") return accum;
const mappedProp = Model.columnName(model, key as any);
if (this.isReserved(mappedProp))
throw new InternalError(`Property name ${mappedProp} is reserved`);
val = val instanceof Date ? new Date(val) : val;
accum[mappedProp] = val;
return accum;
},
{}
);
log.silly(
`Preparing record for ${tableName} table with pk ${(model as any)[pk]}`
);
return {
record: result,
id: (model as any)[pk] as string,
transient: split.transient,
};
}
override revert<M extends Model>(
obj: Record<string, any>,
clazz: Constructor<M>,
id: PrimaryKeyType,
transient?: Record<string, any>,
...args: ContextualArgs<FabricContractContext>
): M {
const { log } = this.logCtx(args, this.revert);
const ob: Record<string, any> = {};
const pk = Model.pk(clazz);
ob[pk as string] = id;
const m = (
typeof clazz === "string" ? Model.build(ob, clazz) : new clazz(ob)
) as M;
log.silly(`Rebuilding model ${m.constructor.name} id ${id}`);
const result = Object.keys(m).reduce((accum: M, key) => {
(accum as Record<string, any>)[key] =
obj[Model.columnName(accum, key as any)];
return accum;
}, m);
if (transient) {
log.debug(
`re-adding transient properties: ${Object.keys(transient).join(", ")}`
);
Object.entries(transient).forEach(([key, val]) => {
if (key in result && (result as any)[key] !== undefined)
throw new InternalError(
`Transient property ${key} already exists on model ${m.constructor.name}. should be impossible`
);
result[key as keyof M] = val;
});
}
return result;
}
override createPrefix<M extends Model>(
tableName: Constructor<M>,
id: PrimaryKeyType,
model: Record<string, any>,
...args: MaybeContextualArg<FabricContractContext>
) {
const { ctxArgs } = this.logCtx(args, this.createPrefix);
const record: Record<string, any> = {};
record[CouchDBKeys.TABLE] = Model.tableName(tableName);
Object.assign(record, model);
return [tableName, id, record, ...ctxArgs] as [
Constructor<M>,
PrimaryKeyType,
Record<string, any>,
...any[],
FabricContractContext,
];
}
override updatePrefix<M extends Model>(
tableName: Constructor<M>,
id: PrimaryKeyType,
model: Record<string, any>,
...args: MaybeContextualArg<FabricContractContext>
): any[] {
const { ctxArgs } = this.logCtx(args, this.updatePrefix);
const record: Record<string, any> = {};
record[CouchDBKeys.TABLE] = Model.tableName(tableName);
Object.assign(record, model);
return [tableName, id, record, ...ctxArgs] as [
Constructor<M>,
PrimaryKeyType,
Record<string, any>,
...any[],
FabricContractContext,
];
}
protected override createAllPrefix<M extends Model>(
tableName: Constructor<M>,
ids: PrimaryKeyType[],
models: Record<string, any>[],
...args: [...any, FabricContractContext]
): (string | string[] | number[] | Record<string, any>[])[] {
if (ids.length !== models.length)
throw new InternalError("Ids and models must have the same length");
const ctx: FabricContractContext = args.pop();
const records = ids.map((id, count) => {
const record: Record<string, any> = {};
record[CouchDBKeys.TABLE] = Model.tableName(tableName);
Object.assign(record, models[count]);
return record;
});
return [tableName, ids, records, ctx as any];
}
protected override updateAllPrefix<M extends Model>(
tableName: Constructor<M>,
ids: PrimaryKeyType[],
models: Record<string, any>[],
...args: [...any, FabricContractContext]
) {
if (ids.length !== models.length)
throw new InternalError("Ids and models must have the same length");
const ctx: FabricContractContext = args.pop();
const records = ids.map((id, count) => {
const record: Record<string, any> = {};
record[CouchDBKeys.TABLE] = Model.tableName(tableName);
Object.assign(record, models[count]);
return record;
});
return [tableName, ids, records, ctx as any];
}
override parseError<E extends BaseError>(
err: Error | string,
reason?: string
): E {
return FabricContractAdapter.parseError(reason || err);
}
protected override logCtx<
ARGS extends any[] = any[],
METHOD extends MethodOrOperation = MethodOrOperation,
>(
args: MaybeContextualArg<FabricContractContext, ARGS>,
operation: METHOD
): FabricContextualizedArgs<ARGS, METHOD extends string ? true : false>;
protected override logCtx<
ARGS extends any[] = any[],
METHOD extends MethodOrOperation = MethodOrOperation,
>(
args: MaybeContextualArg<FabricContractContext, ARGS>,
operation: METHOD,
allowCreate: false
): FabricContextualizedArgs<ARGS, METHOD extends string ? true : false>;
protected override logCtx<
ARGS extends any[] = any[],
METHOD extends MethodOrOperation = MethodOrOperation,
>(
args: MaybeContextualArg<FabricContractContext, ARGS>,
operation: METHOD,
allowCreate: true,
overrides?: Partial<FlagsOf<FabricContractContext>>
): Promise<
FabricContextualizedArgs<ARGS, METHOD extends string ? true : false>
>;
protected override logCtx<
ARGS extends any[] = any[],
METHOD extends MethodOrOperation = MethodOrOperation,
>(
args: MaybeContextualArg<FabricContractContext, ARGS>,
operation: METHOD,
allowCreate: boolean = false,
overrides?: Partial<FlagsOf<FabricContractContext>> | Ctx
):
| Promise<
FabricContextualizedArgs<ARGS, METHOD extends string ? true : false>
>
| FabricContextualizedArgs<ARGS, METHOD extends string ? true : false> {
if (!allowCreate)
return super.logCtx<ARGS, METHOD>(
args,
operation as any,
allowCreate as any,
overrides as any
) as any;
return super.logCtx
.call(this, args, operation as any, allowCreate, overrides as any)
.then((res) => {
if (!(res.ctx instanceof FabricContractContext))
throw new InternalError(`Invalid context binding`);
if (!res.ctx.stub) throw new InternalError(`Missing Stub`);
if (!res.ctx.identity) throw new InternalError(`Missing Identity`);
return Object.assign(res, {
stub: res.ctx.stub,
identity: res.ctx.identity,
});
}) as any;
}
static override parseError<E extends BaseError>(err: Error | string): E {
// if (
// MISSING_PRIVATE_DATA_REGEX.test(
// typeof err === "string" ? err : err.message
// )
// )
// return new UnauthorizedPrivateDataAccess(err) as E;
const msg = typeof err === "string" ? err : err.message;
if (msg.includes(NotFoundError.name)) return new NotFoundError(err) as E;
if (msg.includes(ConflictError.name)) return new ConflictError(err) as E;
if (msg.includes(BadRequestError.name))
return new BadRequestError(err) as E;
if (msg.includes(QueryError.name)) return new QueryError(err) as E;
if (msg.includes(PagingError.name)) return new PagingError(err) as E;
if (msg.includes(UnsupportedError.name))
return new UnsupportedError(err) as E;
if (msg.includes(MigrationError.name)) return new MigrationError(err) as E;
if (msg.includes(ObserverError.name)) return new ObserverError(err) as E;
if (msg.includes(AuthorizationError.name))
return new AuthorizationError(err) as E;
if (msg.includes(ForbiddenError.name)) return new ForbiddenError(err) as E;
if (msg.includes(ConnectionError.name))
return new ConnectionError(err) as E;
if (msg.includes(SerializationError.name))
return new SerializationError(err) as E;
if (msg.includes("no ledger context"))
return new MissingContextError(
`No context found. this can be caused by debugging: ${msg}`
) as E;
return new InternalError(err) as E;
}
/**
* @description Static method for decoration overrides
* @summary Overrides/extends decaf decoration with Fabric-specific functionality
* @static
* @override
* @return {void}
*/
static override decoration(): void {
super.decoration();
Decoration.flavouredAs(FabricFlavour)
.for(PersistenceKeys.CREATED_BY)
.define(
onCreate(createdByOnFabricCreateUpdate),
propMetadata(PersistenceKeys.CREATED_BY, {})
)
.apply();
Decoration.flavouredAs(FabricFlavour)
.for(PersistenceKeys.UPDATED_BY)
.define(
onCreateUpdate(createdByOnFabricCreateUpdate),
propMetadata(PersistenceKeys.UPDATED_BY, {})
)
.apply();
Decoration.flavouredAs(FabricFlavour)
.for(PersistenceKeys.COLUMN)
.extend(FabricProperty())
.apply();
Decoration.flavouredAs(FabricFlavour)
.for(ValidationKeys.DATE)
.extend(function fabricProperty() {
return (target: any, prop?: any) => {
Property(prop, "string:date")(target, prop);
};
});
Decoration.flavouredAs(FabricFlavour)
.for(PersistenceKeys.TABLE)
.extend(function table(obj: any) {
const chain: any[] = [];
let current =
typeof obj === "function"
? Metadata.constr(obj)
: Metadata.constr(obj.constructor);
while (current && current !== Object && current.prototype) {
chain.push(current);
current = Object.getPrototypeOf(current);
}
console.log(chain.map((c) => c.name || c));
// Apply from the base class down to the decorated class
while (chain.length > 0) {
const constructor = chain.pop();
console.log(`Calling on ${constructor.name}`);
FabricObject()(constructor);
}
return FabricObject()(obj);
})
.apply();
}
}
FabricContractAdapter.decoration();
Adapter.setCurrent(FabricFlavour);
Source