diff --git a/examples/src/parking-lot.ts b/examples/src/parking-lot.ts index 023eff757..effa2b72b 100644 --- a/examples/src/parking-lot.ts +++ b/examples/src/parking-lot.ts @@ -32,9 +32,9 @@ class Engine { @NearBindgen({}) class ParkingLot { - cars: LookupMap; + cars: LookupMap; constructor() { - this.cars = new LookupMap('a'); + this.cars = new LookupMap('a'); } @call({}) diff --git a/lib/collections/lookup-map.d.ts b/lib/collections/lookup-map.d.ts index 8ab995dad..0035b4ee0 100644 --- a/lib/collections/lookup-map.d.ts +++ b/lib/collections/lookup-map.d.ts @@ -1,12 +1,13 @@ +import { GetOptions } from '../types/collections'; import { Bytes } from '../utils'; -export declare class LookupMap { +export declare class LookupMap { readonly keyPrefix: Bytes; constructor(keyPrefix: Bytes); containsKey(key: Bytes): boolean; - get(key: Bytes): unknown | null; - remove(key: Bytes): unknown | null; - set(key: Bytes, value: unknown): unknown | null; - extend(objects: [Bytes, unknown][]): void; + get(key: Bytes, options?: GetOptions): DataType | null; + remove(key: Bytes): DataType | null; + set(key: Bytes, value: DataType): DataType | null; + extend(objects: [Bytes, DataType][]): void; serialize(): string; - static reconstruct(data: LookupMap): LookupMap; + static reconstruct(data: LookupMap): LookupMap; } diff --git a/lib/collections/lookup-map.js b/lib/collections/lookup-map.js index 837a46402..a9b331700 100644 --- a/lib/collections/lookup-map.js +++ b/lib/collections/lookup-map.js @@ -7,11 +7,12 @@ export class LookupMap { let storageKey = this.keyPrefix + JSON.stringify(key); return near.storageHasKey(storageKey); } - get(key) { + get(key, options) { let storageKey = this.keyPrefix + JSON.stringify(key); let raw = near.storageRead(storageKey); if (raw !== null) { - return JSON.parse(raw); + const value = JSON.parse(raw); + return !!options?.reconstructor ? options.reconstructor(value) : value; } return null; } diff --git a/lib/collections/unordered-map.d.ts b/lib/collections/unordered-map.d.ts index 76995b588..ab1b06701 100644 --- a/lib/collections/unordered-map.d.ts +++ b/lib/collections/unordered-map.d.ts @@ -1,27 +1,29 @@ import { Bytes } from "../utils"; import { Vector } from "./vector"; import { LookupMap } from "./lookup-map"; -export declare class UnorderedMap { +import { GetOptions } from "../types/collections"; +declare type ValueAndIndex = [value: DataType, index: number]; +export declare class UnorderedMap { readonly prefix: Bytes; - readonly keys: Vector; - readonly values: LookupMap; + readonly keys: Vector; + readonly values: LookupMap>; constructor(prefix: Bytes); get length(): number; isEmpty(): boolean; - get(key: Bytes): unknown | null; - set(key: Bytes, value: unknown): unknown | null; - remove(key: Bytes): unknown | null; + get(key: Bytes, options?: GetOptions): DataType | null; + set(key: Bytes, value: DataType): DataType | null; + remove(key: Bytes): DataType | null; clear(): void; - toArray(): [Bytes, unknown][]; - [Symbol.iterator](): UnorderedMapIterator; - extend(kvs: [Bytes, unknown][]): void; + toArray(): [Bytes, DataType][]; + [Symbol.iterator](): UnorderedMapIterator; + extend(kvs: [Bytes, DataType][]): void; serialize(): string; - static reconstruct(data: UnorderedMap): UnorderedMap; + static reconstruct(data: UnorderedMap): UnorderedMap; } -declare class UnorderedMapIterator { +declare class UnorderedMapIterator { private keys; private map; - constructor(unorderedMap: UnorderedMap); + constructor(unorderedMap: UnorderedMap); next(): { value: [unknown | null, unknown | null]; done: boolean; diff --git a/lib/collections/unordered-map.js b/lib/collections/unordered-map.js index 070f74e62..673723823 100644 --- a/lib/collections/unordered-map.js +++ b/lib/collections/unordered-map.js @@ -15,13 +15,13 @@ export class UnorderedMap { let keysIsEmpty = this.keys.isEmpty(); return keysIsEmpty; } - get(key) { + get(key, options) { let valueAndIndex = this.values.get(key); if (valueAndIndex === null) { return null; } let value = valueAndIndex[0]; - return value; + return !!options?.reconstructor ? options.reconstructor(value) : value; } set(key, value) { let valueAndIndex = this.values.get(key); diff --git a/lib/collections/unordered-set.d.ts b/lib/collections/unordered-set.d.ts index b7de5f035..fbdf65f62 100644 --- a/lib/collections/unordered-set.d.ts +++ b/lib/collections/unordered-set.d.ts @@ -1,19 +1,19 @@ import { Bytes } from "../utils"; import { Vector } from "./vector"; -export declare class UnorderedSet { +export declare class UnorderedSet { readonly prefix: Bytes; readonly elementIndexPrefix: Bytes; - readonly elements: Vector; + readonly elements: Vector; constructor(prefix: Bytes); get length(): number; isEmpty(): boolean; - contains(element: unknown): boolean; - set(element: unknown): boolean; - remove(element: unknown): boolean; + contains(element: DataType): boolean; + set(element: DataType): boolean; + remove(element: DataType): boolean; clear(): void; toArray(): Bytes[]; - [Symbol.iterator](): import("./vector").VectorIterator; - extend(elements: unknown[]): void; + [Symbol.iterator](): import("./vector").VectorIterator; + extend(elements: DataType[]): void; serialize(): string; - static reconstruct(data: UnorderedSet): UnorderedSet; + static reconstruct(data: UnorderedSet): UnorderedSet; } diff --git a/lib/collections/vector.d.ts b/lib/collections/vector.d.ts index 70e5a54b5..07419d9d0 100644 --- a/lib/collections/vector.d.ts +++ b/lib/collections/vector.d.ts @@ -1,25 +1,26 @@ import { Bytes } from "../utils"; -export declare class Vector { +import { GetOptions } from '../types/collections'; +export declare class Vector { length: number; readonly prefix: Bytes; constructor(prefix: Bytes); isEmpty(): boolean; - get(index: number): unknown | null; + get(index: number, options?: GetOptions): DataType | null; swapRemove(index: number): unknown | null; - push(element: unknown): void; - pop(): unknown | null; - replace(index: number, element: unknown): unknown; - extend(elements: unknown[]): void; - [Symbol.iterator](): VectorIterator; + push(element: DataType): void; + pop(): DataType | null; + replace(index: number, element: DataType): DataType; + extend(elements: DataType[]): void; + [Symbol.iterator](): VectorIterator; clear(): void; - toArray(): unknown[]; + toArray(): DataType[]; serialize(): string; - static reconstruct(data: Vector): Vector; + static reconstruct(data: Vector): Vector; } -export declare class VectorIterator { +export declare class VectorIterator { private current; private vector; - constructor(vector: Vector); + constructor(vector: Vector); next(): { value: unknown | null; done: boolean; diff --git a/lib/collections/vector.js b/lib/collections/vector.js index e9548621d..17cedc66c 100644 --- a/lib/collections/vector.js +++ b/lib/collections/vector.js @@ -18,12 +18,13 @@ export class Vector { isEmpty() { return this.length == 0; } - get(index) { + get(index, options) { if (index >= this.length) { return null; } let storageKey = indexToKey(this.prefix, index); - return JSON.parse(near.storageRead(storageKey)); + const value = JSON.parse(near.storageRead(storageKey)); + return !!options?.reconstructor ? options.reconstructor(value) : value; } /// Removes an element from the vector and returns it in serialized form. /// The removed element is replaced by the last element of the vector. diff --git a/lib/types/collections.d.ts b/lib/types/collections.d.ts new file mode 100644 index 000000000..71a006ccc --- /dev/null +++ b/lib/types/collections.d.ts @@ -0,0 +1,3 @@ +export declare type GetOptions = { + reconstructor?: (value: unknown) => DataType; +}; diff --git a/lib/types/collections.js b/lib/types/collections.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/lib/types/collections.js @@ -0,0 +1 @@ +export {}; diff --git a/src/collections/lookup-map.ts b/src/collections/lookup-map.ts index b934adb29..298f77795 100644 --- a/src/collections/lookup-map.ts +++ b/src/collections/lookup-map.ts @@ -1,7 +1,8 @@ import * as near from '../api' +import { GetOptions } from '../types/collections'; import { Bytes } from '../utils'; -export class LookupMap { +export class LookupMap { readonly keyPrefix: Bytes; constructor(keyPrefix: Bytes) { @@ -13,16 +14,17 @@ export class LookupMap { return near.storageHasKey(storageKey) } - get(key: Bytes): unknown | null { + get(key: Bytes, options?: GetOptions): DataType | null { let storageKey = this.keyPrefix + JSON.stringify(key) let raw = near.storageRead(storageKey) if (raw !== null) { - return JSON.parse(raw) + const value = JSON.parse(raw) + return !!options?.reconstructor ? options.reconstructor(value) : value as DataType } return null } - remove(key: Bytes): unknown | null { + remove(key: Bytes): DataType | null { let storageKey = this.keyPrefix + JSON.stringify(key) if (near.storageRemove(storageKey)) { return JSON.parse(near.storageGetEvicted()) @@ -30,7 +32,7 @@ export class LookupMap { return null } - set(key: Bytes, value: unknown): unknown | null { + set(key: Bytes, value: DataType): DataType | null { let storageKey = this.keyPrefix + JSON.stringify(key) let storageValue = JSON.stringify(value) if (near.storageWrite(storageKey, storageValue)) { @@ -39,7 +41,7 @@ export class LookupMap { return null } - extend(objects: [Bytes, unknown][]) { + extend(objects: [Bytes, DataType][]) { for (let kv of objects) { this.set(kv[0], kv[1]) } @@ -50,7 +52,7 @@ export class LookupMap { } // converting plain object to class object - static reconstruct(data: LookupMap): LookupMap { + static reconstruct(data: LookupMap): LookupMap { return new LookupMap(data.keyPrefix) } } \ No newline at end of file diff --git a/src/collections/lookup-set.ts b/src/collections/lookup-set.ts index e8927e219..777fdb3a5 100644 --- a/src/collections/lookup-set.ts +++ b/src/collections/lookup-set.ts @@ -1,3 +1,5 @@ + + import * as near from '../api' import { Bytes } from '../utils'; diff --git a/src/collections/unordered-map.ts b/src/collections/unordered-map.ts index dcd8a681d..b69c55465 100644 --- a/src/collections/unordered-map.ts +++ b/src/collections/unordered-map.ts @@ -1,21 +1,22 @@ import { Bytes, Mutable } from "../utils"; import { Vector, VectorIterator } from "./vector"; import { LookupMap } from "./lookup-map"; +import { GetOptions } from "../types/collections"; const ERR_INCONSISTENT_STATE = "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"; -type ValueAndIndex = [value: unknown, index: number] +type ValueAndIndex = [value: DataType, index: number] -export class UnorderedMap { +export class UnorderedMap { readonly prefix: Bytes; - readonly keys: Vector; - readonly values: LookupMap; + readonly keys: Vector; + readonly values: LookupMap>; constructor(prefix: Bytes) { this.prefix = prefix; - this.keys = new Vector(prefix + 'u'); // intentional different prefix with old UnorderedMap - this.values = new LookupMap(prefix + 'm'); + this.keys = new Vector(prefix + 'u'); // intentional different prefix with old UnorderedMap + this.values = new LookupMap>(prefix + 'm'); } get length() { @@ -28,22 +29,22 @@ export class UnorderedMap { return keysIsEmpty; } - get(key: Bytes): unknown | null { + get(key: Bytes, options?: GetOptions): DataType | null { let valueAndIndex = this.values.get(key); if (valueAndIndex === null) { return null; } - let value = (valueAndIndex as ValueAndIndex)[0]; - return value; + let value = (valueAndIndex as ValueAndIndex)[0]; + return !!options?.reconstructor ? options.reconstructor(value) : value; } - set(key: Bytes, value: unknown): unknown | null { + set(key: Bytes, value: DataType): DataType | null { let valueAndIndex = this.values.get(key); if (valueAndIndex !== null) { - let oldValue = (valueAndIndex as ValueAndIndex)[0]; - (valueAndIndex as ValueAndIndex)[0] = value; + let oldValue = (valueAndIndex as ValueAndIndex)[0]; + (valueAndIndex as ValueAndIndex)[0] = value; this.values.set(key, valueAndIndex) - return oldValue; + return oldValue as DataType; } let nextIndex = this.length; @@ -52,12 +53,12 @@ export class UnorderedMap { return null; } - remove(key: Bytes): unknown | null { + remove(key: Bytes): DataType | null { let oldValueAndIndex = this.values.remove(key); if (oldValueAndIndex === null) { return null; } - let index = (oldValueAndIndex as ValueAndIndex)[1]; + let index = (oldValueAndIndex as ValueAndIndex)[1]; if (this.keys.swapRemove(index) === null) { throw new Error(ERR_INCONSISTENT_STATE); } @@ -72,7 +73,7 @@ export class UnorderedMap { } this.values.set(swappedKey, [swappedValueAndIndex[0], index]) } - return (oldValueAndIndex as ValueAndIndex)[0]; + return (oldValueAndIndex as ValueAndIndex)[0]; } clear() { @@ -83,7 +84,7 @@ export class UnorderedMap { this.keys.clear(); } - toArray(): [Bytes, unknown][] { + toArray(): [Bytes, DataType][] { let ret = []; for (let v of this) { ret.push(v); @@ -91,39 +92,39 @@ export class UnorderedMap { return ret; } - [Symbol.iterator](): UnorderedMapIterator { - return new UnorderedMapIterator(this); + [Symbol.iterator](): UnorderedMapIterator { + return new UnorderedMapIterator(this); } - extend(kvs: [Bytes, unknown][]) { + extend(kvs: [Bytes, DataType][]) { for (let [k, v] of kvs) { this.set(k, v); } } serialize(): string { - return JSON.stringify(this) + return JSON.stringify(this); } // converting plain object to class object - static reconstruct(data: UnorderedMap): UnorderedMap { + static reconstruct(data: UnorderedMap): UnorderedMap { // removing readonly modifier - type MutableUnorderedMap = Mutable; + type MutableUnorderedMap = Mutable>; let map = new UnorderedMap(data.prefix) as MutableUnorderedMap; // reconstruct keys Vector map.keys = new Vector(data.prefix + "u"); map.keys.length = data.keys.length; // reconstruct values LookupMap map.values = new LookupMap(data.prefix + "m"); - return map as UnorderedMap; + return map as UnorderedMap; } } -class UnorderedMapIterator { - private keys: VectorIterator; - private map: LookupMap; +class UnorderedMapIterator { + private keys: VectorIterator; + private map: LookupMap>; - constructor(unorderedMap: UnorderedMap) { + constructor(unorderedMap: UnorderedMap) { this.keys = new VectorIterator(unorderedMap.keys); this.map = unorderedMap.values; } diff --git a/src/collections/unordered-set.ts b/src/collections/unordered-set.ts index 5f72180cc..78c47bf99 100644 --- a/src/collections/unordered-set.ts +++ b/src/collections/unordered-set.ts @@ -18,10 +18,10 @@ function deserializeIndex(rawIndex: Bytes): number { return data[0]; } -export class UnorderedSet { +export class UnorderedSet { readonly prefix: Bytes; readonly elementIndexPrefix: Bytes; - readonly elements: Vector; + readonly elements: Vector; constructor(prefix: Bytes) { this.prefix = prefix; @@ -38,12 +38,12 @@ export class UnorderedSet { return this.elements.isEmpty(); } - contains(element: unknown): boolean { + contains(element: DataType): boolean { let indexLookup = this.elementIndexPrefix + JSON.stringify(element); return near.storageHasKey(indexLookup); } - set(element: unknown): boolean { + set(element: DataType): boolean { let indexLookup = this.elementIndexPrefix + JSON.stringify(element); if (near.storageRead(indexLookup)) { return false; @@ -56,7 +56,7 @@ export class UnorderedSet { } } - remove(element: unknown): boolean { + remove(element: DataType): boolean { let indexLookup = this.elementIndexPrefix + JSON.stringify(element); let indexRaw = near.storageRead(indexLookup); if (indexRaw) { @@ -75,7 +75,8 @@ export class UnorderedSet { // If the removed element was the last element from keys, then we don't need to // reinsert the lookup back. if (lastElement != element) { - let lastLookupElement = this.elementIndexPrefix + JSON.stringify(lastElement); + let lastLookupElement = + this.elementIndexPrefix + JSON.stringify(lastElement); near.storageWrite(lastLookupElement, indexRaw); } } @@ -106,25 +107,25 @@ export class UnorderedSet { return this.elements[Symbol.iterator](); } - extend(elements: unknown[]) { + extend(elements: DataType[]) { for (let element of elements) { this.set(element); } } serialize(): string { - return JSON.stringify(this) + return JSON.stringify(this); } // converting plain object to class object - static reconstruct(data: UnorderedSet): UnorderedSet { + static reconstruct(data: UnorderedSet): UnorderedSet { // removing readonly modifier - type MutableUnorderedSet = Mutable; + type MutableUnorderedSet = Mutable>; let set = new UnorderedSet(data.prefix) as MutableUnorderedSet; // reconstruct Vector let elementsPrefix = data.prefix + "e"; set.elements = new Vector(elementsPrefix); set.elements.length = data.elements.length; - return set as UnorderedSet; + return set as UnorderedSet; } } diff --git a/src/collections/vector.ts b/src/collections/vector.ts index d7036a65f..0d566377d 100644 --- a/src/collections/vector.ts +++ b/src/collections/vector.ts @@ -1,6 +1,6 @@ import * as near from "../api"; import { Bytes, u8ArrayToBytes } from "../utils"; - +import {GetOptions} from '../types/collections' const ERR_INDEX_OUT_OF_BOUNDS = "Index out of bounds"; const ERR_INCONSISTENT_STATE = "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"; @@ -14,7 +14,7 @@ function indexToKey(prefix: Bytes, index: number): Bytes { /// An iterable implementation of vector that stores its content on the trie. /// Uses the following map: index -> element -export class Vector { +export class Vector { length: number; readonly prefix: Bytes; @@ -27,12 +27,13 @@ export class Vector { return this.length == 0; } - get(index: number): unknown | null { + get(index: number, options?: GetOptions): DataType | null { if (index >= this.length) { return null; } let storageKey = indexToKey(this.prefix, index); - return JSON.parse(near.storageRead(storageKey)); + const value = JSON.parse(near.storageRead(storageKey)) + return !!options?.reconstructor ? options.reconstructor(value) : value as DataType } /// Removes an element from the vector and returns it in serialized form. @@ -54,13 +55,13 @@ export class Vector { } } - push(element: unknown) { + push(element: DataType) { let key = indexToKey(this.prefix, this.length); this.length += 1; near.storageWrite(key, JSON.stringify(element)); } - pop(): unknown | null { + pop(): DataType | null { if (this.isEmpty()) { return null; } else { @@ -75,7 +76,7 @@ export class Vector { } } - replace(index: number, element: unknown): unknown { + replace(index: number, element: DataType): DataType { if (index >= this.length) { throw new Error(ERR_INDEX_OUT_OF_BOUNDS); } else { @@ -88,13 +89,13 @@ export class Vector { } } - extend(elements: unknown[]) { + extend(elements: DataType[]) { for (let element of elements) { this.push(element); } } - [Symbol.iterator](): VectorIterator { + [Symbol.iterator](): VectorIterator { return new VectorIterator(this); } @@ -106,7 +107,7 @@ export class Vector { this.length = 0; } - toArray(): unknown[] { + toArray(): DataType[] { let ret = []; for (let v of this) { ret.push(v); @@ -115,21 +116,21 @@ export class Vector { } serialize(): string { - return JSON.stringify(this) + return JSON.stringify(this); } // converting plain object to class object - static reconstruct(data: Vector): Vector { - let vector = new Vector(data.prefix); + static reconstruct(data: Vector): Vector { + let vector = new Vector(data.prefix); vector.length = data.length; return vector; } } -export class VectorIterator { +export class VectorIterator { private current: number; - private vector: Vector; - constructor(vector: Vector) { + private vector: Vector; + constructor(vector: Vector) { this.current = 0; this.vector = vector; } diff --git a/src/types/collections.ts b/src/types/collections.ts new file mode 100644 index 000000000..fb727c2ff --- /dev/null +++ b/src/types/collections.ts @@ -0,0 +1,3 @@ +export type GetOptions = { + reconstructor?: (value: unknown) => DataType +} \ No newline at end of file diff --git a/tests/src/unordered-map.js b/tests/src/unordered-map.js index 9c8978012..444e1e5da 100644 --- a/tests/src/unordered-map.js +++ b/tests/src/unordered-map.js @@ -3,6 +3,7 @@ import { call, view, UnorderedMap, + near, } from 'near-sdk-js' import { House, Room } from './model.js'; @@ -59,10 +60,8 @@ class UnorderedMapTestContract { @view({}) get_house() { - const rawHouse = this.unorderedMap.get('house1') - const house = new House(rawHouse.name, rawHouse.rooms) - const rawRoom = house.rooms[0] - const room = new Room(rawRoom.name, rawRoom.size) + const house = this.unorderedMap.get('house1', {reconstructor: (rawHouse) => new House(rawHouse.name, rawHouse.rooms.map((rawRoom) => new Room(rawRoom.name, rawRoom.size)))}) + const room = house.rooms[0] return house.describe() + room.describe() } } diff --git a/yarn.lock b/yarn.lock index d04f0faf9..f331fc3d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1032,4 +1032,4 @@ yargs@^17.5.1: require-directory "^2.1.1" string-width "^4.2.3" y18n "^5.0.5" - yargs-parser "^21.0.0" + yargs-parser "^21.0.0" \ No newline at end of file