From 61ad71fbf34091833cac6fe8cb156ef6c863912a Mon Sep 17 00:00:00 2001 From: Spring Chiu Date: Sun, 7 Jan 2024 16:08:14 +0800 Subject: [PATCH 1/6] feat: update reconstruct api --- examples/src/status-deserialize-class.js | 27 +++++++++++++------ .../near-sdk-js/lib/collections/subtype.js | 19 ++++++++----- packages/near-sdk-js/lib/utils.js | 12 ++++++--- .../near-sdk-js/src/collections/subtype.ts | 20 ++++++++------ packages/near-sdk-js/src/utils.ts | 11 +++++--- 5 files changed, 58 insertions(+), 31 deletions(-) diff --git a/examples/src/status-deserialize-class.js b/examples/src/status-deserialize-class.js index 270cd1b8..db8d7fcc 100644 --- a/examples/src/status-deserialize-class.js +++ b/examples/src/status-deserialize-class.js @@ -28,7 +28,8 @@ class Truck { static schema = { name: "string", speed: "number", - loads: {collection: {reconstructor: UnorderedMap.reconstruct, value: 'string'}} + // loads: {collection: {reconstructor: UnorderedMap.reconstruct, value: 'string'}} + loads: {class: UnorderedMap } }; constructor() { this.name = ""; @@ -41,6 +42,9 @@ class Truck { } } +// sdk should first try if UnorderedMap has a static schema and use it to recursively decode. +// In this case, UnorderedMap doesn't. +// So sdk should next try call UnorderedMap.reconstruct. @NearBindgen({}) export class StatusDeserializeClass { static schema = { @@ -48,13 +52,20 @@ export class StatusDeserializeClass { records: {map: { key: 'string', value: 'string' }}, truck: Truck, messages: {array: {value: 'string'}}, - efficient_recordes: {collection: {reconstructor: UnorderedMap.reconstruct, value: 'string'}}, - nested_efficient_recordes: {collection: {reconstructor: UnorderedMap.reconstruct, value: { collection: {reconstructor: UnorderedMap.reconstruct, value: 'string'}}}}, - nested_lookup_recordes: {collection: {reconstructor: UnorderedMap.reconstruct, value: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}}}}, - vector_nested_group: {collection: {reconstructor: Vector.reconstruct, value: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}}}}, - lookup_nest_vec: {collection: {reconstructor: LookupMap.reconstruct, value: { collection: { reconstructor: Vector.reconstruct, value: 'string' }}}}, - unordered_set: {collection: {reconstructor: UnorderedSet.reconstruct, value: 'string'}}, - user_car_map: {collection: {reconstructor: UnorderedMap.reconstruct, value: Car }}, + // efficient_recordes: {class: {reconstructor: UnorderedMap.reconstruct, value: 'string'}}, + efficient_recordes: {class: UnorderedMap}, + // nested_efficient_recordes: {collection: {reconstructor: UnorderedMap.reconstruct, value: { collection: {reconstructor: UnorderedMap.reconstruct, value: 'string'}}}}, + nested_efficient_recordes: {class: UnorderedMap, value: UnorderedMap}, + // nested_lookup_recordes: {collection: {reconstructor: UnorderedMap.reconstruct, value: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}}}}, + nested_lookup_recordes: {class: UnorderedMap, value: {class: LookupMap }}, + // vector_nested_group: {collection: {reconstructor: Vector.reconstruct, value: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}}}}, + vector_nested_group: {class: Vector, value: { class: LookupMap }}, + // lookup_nest_vec: {collection: {reconstructor: LookupMap.reconstruct, value: { collection: { reconstructor: Vector.reconstruct, value: 'string' }}}}, + lookup_nest_vec: { class: LookupMap, value: Vector }, + // unordered_set: {collection: {reconstructor: UnorderedSet.reconstruct, value: 'string'}}, + unordered_set: {class: UnorderedSet }, + // user_car_map: {collection: {reconstructor: UnorderedMap.reconstruct, value: Car }}, + user_car_map: {class: UnorderedMap, value: Car }, big_num: 'bigint', date: 'date' }; diff --git a/packages/near-sdk-js/lib/collections/subtype.js b/packages/near-sdk-js/lib/collections/subtype.js index af87b68c..0b4ee206 100644 --- a/packages/near-sdk-js/lib/collections/subtype.js +++ b/packages/near-sdk-js/lib/collections/subtype.js @@ -8,14 +8,19 @@ export class SubType { } const subtype = this.subtype(); if (options.reconstructor == undefined && - subtype != undefined && + subtype != undefined) { + if ( // eslint-disable-next-line no-prototype-builtins - subtype.hasOwnProperty("collection") && - typeof this.subtype().collection.reconstructor === "function") { - // { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - options.reconstructor = this.subtype().collection.reconstructor; + subtype.hasOwnProperty("class") && + typeof subtype.class.reconstructor === "function") { + // { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + options.reconstructor = subtype.class.reconstructor; + } + else if (subtype.reconstructor === "function") { + options.reconstructor = subtype.reconstructor; + } } return options; } diff --git a/packages/near-sdk-js/lib/utils.js b/packages/near-sdk-js/lib/utils.js index 395c825b..78ef3df5 100644 --- a/packages/near-sdk-js/lib/utils.js +++ b/packages/near-sdk-js/lib/utils.js @@ -138,7 +138,7 @@ export function deserialize(valueToDeserialize) { }); } export function decodeObj2class(class_instance, obj) { - if (typeof obj != "object" || + if (typeof obj != "object" || typeof obj === "bigint" || obj instanceof Date || class_instance.constructor.schema === undefined) { return obj; } @@ -174,16 +174,20 @@ export function decodeObj2class(class_instance, obj) { } // eslint-disable-next-line no-prototype-builtins } - else if (ty !== undefined && ty.hasOwnProperty("collection")) { + else if (ty !== undefined && ty.hasOwnProperty("class")) { // nested_lookup_recordes: {collection: {reconstructor: UnorderedMap.reconstruct, value: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}}}}, // {collection: {reconstructor: - class_instance[key] = ty["collection"]["reconstructor"](obj[key]); - const subtype_value = ty["collection"]["value"]; + class_instance[key] = ty["class"].reconstruct(obj[key]); + const subtype_value = ty["value"]; class_instance[key].subtype = function () { // example: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + // example: UnorderedMap return subtype_value; }; } + else if (ty !== undefined && typeof ty.reconstruct === "function") { + class_instance[key] = ty.reconstruct(obj[key]); + } else { // normal case with nested Class, such as field is truck: Truck, class_instance[key] = decodeObj2class(class_instance[key], obj[key]); diff --git a/packages/near-sdk-js/src/collections/subtype.ts b/packages/near-sdk-js/src/collections/subtype.ts index 1b180162..07cd5944 100644 --- a/packages/near-sdk-js/src/collections/subtype.ts +++ b/packages/near-sdk-js/src/collections/subtype.ts @@ -14,15 +14,19 @@ export abstract class SubType { const subtype = this.subtype(); if ( options.reconstructor == undefined && - subtype != undefined && - // eslint-disable-next-line no-prototype-builtins - subtype.hasOwnProperty("collection") && - typeof this.subtype().collection.reconstructor === "function" + subtype != undefined ) { - // { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - options.reconstructor = this.subtype().collection.reconstructor; + if ( + // eslint-disable-next-line no-prototype-builtins + subtype.hasOwnProperty("class") && + typeof subtype.class.reconstructor === "function") { + // { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + options.reconstructor = subtype.class.reconstructor; + } else if (subtype.reconstructor === "function") { + options.reconstructor = subtype.reconstructor; + } } return options; } diff --git a/packages/near-sdk-js/src/utils.ts b/packages/near-sdk-js/src/utils.ts index 7611831b..b211fab9 100644 --- a/packages/near-sdk-js/src/utils.ts +++ b/packages/near-sdk-js/src/utils.ts @@ -204,7 +204,7 @@ export function deserialize(valueToDeserialize: Uint8Array): unknown { export function decodeObj2class(class_instance, obj) { if ( - typeof obj != "object" || + typeof obj != "object" || typeof obj === "bigint" || obj instanceof Date || class_instance.constructor.schema === undefined ) { return obj; @@ -242,15 +242,18 @@ export function decodeObj2class(class_instance, obj) { } } // eslint-disable-next-line no-prototype-builtins - } else if (ty !== undefined && ty.hasOwnProperty("collection")) { + } else if (ty !== undefined && ty.hasOwnProperty("class")) { // nested_lookup_recordes: {collection: {reconstructor: UnorderedMap.reconstruct, value: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}}}}, // {collection: {reconstructor: - class_instance[key] = ty["collection"]["reconstructor"](obj[key]); - const subtype_value = ty["collection"]["value"]; + class_instance[key] = ty["class"].reconstruct(obj[key]); + const subtype_value = ty["value"]; class_instance[key].subtype = function () { // example: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + // example: UnorderedMap return subtype_value; }; + } else if (ty !== undefined && typeof ty.reconstruct === "function") { + class_instance[key] = ty.reconstruct(obj[key]); } else { // normal case with nested Class, such as field is truck: Truck, class_instance[key] = decodeObj2class(class_instance[key], obj[key]); From 2ffdc8c539cd075997f90f04960d9a7d3a59ce53 Mon Sep 17 00:00:00 2001 From: Spring Chiu Date: Fri, 26 Jan 2024 03:06:42 +0800 Subject: [PATCH 2/6] fix: fix convert subtype collection class failed --- .../test-status-deserialize-class.ava.js | 18 ----------- .../near-sdk-js/lib/collections/subtype.js | 12 ++++---- packages/near-sdk-js/lib/utils.js | 30 +++++++++++-------- .../near-sdk-js/src/collections/subtype.ts | 19 +++++------- packages/near-sdk-js/src/utils.ts | 30 +++++++++++-------- 5 files changed, 47 insertions(+), 62 deletions(-) diff --git a/examples/__tests__/test-status-deserialize-class.ava.js b/examples/__tests__/test-status-deserialize-class.ava.js index f52d92d2..43bb6974 100644 --- a/examples/__tests__/test-status-deserialize-class.ava.js +++ b/examples/__tests__/test-status-deserialize-class.ava.js @@ -149,21 +149,3 @@ test("Ali set_extra_record without schema defined then gets", async (t) => { const recordWithoutSchemaDefined = await statusMessage.view("get_extra_record", { account_id: ali.accountId }); t.is(recordWithoutSchemaDefined, "Hello world!"); }); - -test("View get_subtype_of_efficient_recordes", async (t) => { - const { statusMessage } = t.context.accounts; - - t.is( - await statusMessage.view("get_subtype_of_efficient_recordes", { }), - 'string' - ); -}); - -test("View get_subtype_of_nested_efficient_recordes", async (t) => { - const { statusMessage } = t.context.accounts; - - t.is( - JSON.stringify(await statusMessage.view("get_subtype_of_nested_efficient_recordes", { })), - '{"collection":{"value":"string"}}' - ); -}); \ No newline at end of file diff --git a/packages/near-sdk-js/lib/collections/subtype.js b/packages/near-sdk-js/lib/collections/subtype.js index 0b4ee206..24d31f65 100644 --- a/packages/near-sdk-js/lib/collections/subtype.js +++ b/packages/near-sdk-js/lib/collections/subtype.js @@ -7,19 +7,17 @@ export class SubType { options = {}; } const subtype = this.subtype(); - if (options.reconstructor == undefined && - subtype != undefined) { + if (options.reconstructor == undefined && subtype != undefined) { if ( // eslint-disable-next-line no-prototype-builtins subtype.hasOwnProperty("class") && - typeof subtype.class.reconstructor === "function") { - // { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + typeof subtype.class.reconstruct === "function") { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - options.reconstructor = subtype.class.reconstructor; + options.reconstructor = subtype.class.reconstruct; } - else if (subtype.reconstructor === "function") { - options.reconstructor = subtype.reconstructor; + else if (typeof subtype.reconstruct === "function") { + options.reconstructor = subtype.reconstruct; } } return options; diff --git a/packages/near-sdk-js/lib/utils.js b/packages/near-sdk-js/lib/utils.js index 78ef3df5..93a1ca28 100644 --- a/packages/near-sdk-js/lib/utils.js +++ b/packages/near-sdk-js/lib/utils.js @@ -47,14 +47,14 @@ export function getValueWithOptions(subDatatype, value, options = { const collection = options.reconstructor(deserialized); if (subDatatype !== undefined && // eslint-disable-next-line no-prototype-builtins - subDatatype.hasOwnProperty("collection") && + subDatatype.hasOwnProperty("class") && // eslint-disable-next-line no-prototype-builtins - subDatatype["collection"].hasOwnProperty("value")) { + subDatatype["class"].hasOwnProperty("value")) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore collection.subtype = function () { - // example: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} - return subDatatype["collection"]["value"]; + // example: {class: UnorderedMap, value: UnorderedMap} + return subDatatype["class"]["value"]; }; } return collection; @@ -138,7 +138,9 @@ export function deserialize(valueToDeserialize) { }); } export function decodeObj2class(class_instance, obj) { - if (typeof obj != "object" || typeof obj === "bigint" || obj instanceof Date || + if (typeof obj != "object" || + typeof obj === "bigint" || + obj instanceof Date || class_instance.constructor.schema === undefined) { return obj; } @@ -175,15 +177,17 @@ export function decodeObj2class(class_instance, obj) { // eslint-disable-next-line no-prototype-builtins } else if (ty !== undefined && ty.hasOwnProperty("class")) { - // nested_lookup_recordes: {collection: {reconstructor: UnorderedMap.reconstruct, value: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}}}}, - // {collection: {reconstructor: + // => nested_lookup_recordes: {class: UnorderedMap, value: {class: LookupMap }}, class_instance[key] = ty["class"].reconstruct(obj[key]); - const subtype_value = ty["value"]; - class_instance[key].subtype = function () { - // example: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} - // example: UnorderedMap - return subtype_value; - }; + // eslint-disable-next-line no-prototype-builtins + if (ty.hasOwnProperty("value")) { + const subtype_value = ty["value"]; + class_instance[key].subtype = function () { + // example: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + // example: UnorderedMap or {class: UnorderedMap, value: 'string'} + return subtype_value; + }; + } } else if (ty !== undefined && typeof ty.reconstruct === "function") { class_instance[key] = ty.reconstruct(obj[key]); diff --git a/packages/near-sdk-js/src/collections/subtype.ts b/packages/near-sdk-js/src/collections/subtype.ts index 07cd5944..86f281bb 100644 --- a/packages/near-sdk-js/src/collections/subtype.ts +++ b/packages/near-sdk-js/src/collections/subtype.ts @@ -12,20 +12,17 @@ export abstract class SubType { options = {}; } const subtype = this.subtype(); - if ( - options.reconstructor == undefined && - subtype != undefined - ) { + if (options.reconstructor == undefined && subtype != undefined) { if ( - // eslint-disable-next-line no-prototype-builtins - subtype.hasOwnProperty("class") && - typeof subtype.class.reconstructor === "function") { - // { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + // eslint-disable-next-line no-prototype-builtins + subtype.hasOwnProperty("class") && + typeof subtype.class.reconstruct === "function" + ) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - options.reconstructor = subtype.class.reconstructor; - } else if (subtype.reconstructor === "function") { - options.reconstructor = subtype.reconstructor; + options.reconstructor = subtype.class.reconstruct; + } else if (typeof subtype.reconstruct === "function") { + options.reconstructor = subtype.reconstruct; } } return options; diff --git a/packages/near-sdk-js/src/utils.ts b/packages/near-sdk-js/src/utils.ts index b211fab9..90b57e04 100644 --- a/packages/near-sdk-js/src/utils.ts +++ b/packages/near-sdk-js/src/utils.ts @@ -90,15 +90,15 @@ export function getValueWithOptions( if ( subDatatype !== undefined && // eslint-disable-next-line no-prototype-builtins - subDatatype.hasOwnProperty("collection") && + subDatatype.hasOwnProperty("class") && // eslint-disable-next-line no-prototype-builtins - subDatatype["collection"].hasOwnProperty("value") + subDatatype["class"].hasOwnProperty("value") ) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore collection.subtype = function () { - // example: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} - return subDatatype["collection"]["value"]; + // example: {class: UnorderedMap, value: UnorderedMap} + return subDatatype["class"]["value"]; }; } return collection; @@ -204,7 +204,9 @@ export function deserialize(valueToDeserialize: Uint8Array): unknown { export function decodeObj2class(class_instance, obj) { if ( - typeof obj != "object" || typeof obj === "bigint" || obj instanceof Date || + typeof obj != "object" || + typeof obj === "bigint" || + obj instanceof Date || class_instance.constructor.schema === undefined ) { return obj; @@ -243,15 +245,17 @@ export function decodeObj2class(class_instance, obj) { } // eslint-disable-next-line no-prototype-builtins } else if (ty !== undefined && ty.hasOwnProperty("class")) { - // nested_lookup_recordes: {collection: {reconstructor: UnorderedMap.reconstruct, value: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}}}}, - // {collection: {reconstructor: + // => nested_lookup_recordes: {class: UnorderedMap, value: {class: LookupMap }}, class_instance[key] = ty["class"].reconstruct(obj[key]); - const subtype_value = ty["value"]; - class_instance[key].subtype = function () { - // example: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} - // example: UnorderedMap - return subtype_value; - }; + // eslint-disable-next-line no-prototype-builtins + if (ty.hasOwnProperty("value")) { + const subtype_value = ty["value"]; + class_instance[key].subtype = function () { + // example: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + // example: UnorderedMap or {class: UnorderedMap, value: 'string'} + return subtype_value; + }; + } } else if (ty !== undefined && typeof ty.reconstruct === "function") { class_instance[key] = ty.reconstruct(obj[key]); } else { From 7e1c589e6641a3d4d9be5ca77f2bec95ead4c09f Mon Sep 17 00:00:00 2001 From: Spring Chiu Date: Fri, 26 Jan 2024 23:53:49 +0800 Subject: [PATCH 3/6] chore: remove comment and update doc of auto reconstruct by json schema --- AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md | 87 ++++++++++++------------ examples/src/status-deserialize-class.js | 7 -- 2 files changed, 44 insertions(+), 50 deletions(-) diff --git a/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md b/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md index b5a9d97c..791d45fa 100644 --- a/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md +++ b/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md @@ -58,46 +58,49 @@ add_a_car(car: Car) { * for `array` type, we need to declare it in the format of `{array: {value: valueType}}` * for `map` type, we need to declare it in the format of `{map: {key: 'KeyType', value: 'valueType'}}` * Custom Class types: `Car` or any class types -* Near collection types: `Vector`, `LookupMap`, `LookupSet`, `UnorderedMap`, `UnorderedSet` +* Near collection class types: `Vector`, `LookupMap`, `LookupSet`, `UnorderedMap`, `UnorderedSet`, need to declare in the format of `{class: ClassType, value: ValueType}` + * we can ignore `value` field if we don't need to auto reconstruct value to specific class. we often ignore `value` field if value tye are `string`, `number`, `boolean` We have a test example which contains all those types in one schema: [status-deserialize-class.js](./examples/src/status-deserialize-class.js) ```js -class StatusDeserializeClass { - static schema = { - is_inited: "boolean", - records: {map: {key: 'string', value: 'string'}}, - car: Car, - messages: {array: {value: 'string'}}, - efficient_recordes: {unordered_map: {value: 'string'}}, - nested_efficient_recordes: {unordered_map: {value: {unordered_map: {value: 'string'}}}}, - nested_lookup_recordes: {unordered_map: {value: {lookup_map: {value: 'string'}}}}, - vector_nested_group: {vector: {value: {lookup_map: {value: 'string'}}}}, - lookup_nest_vec: {lookup_map: {value: {vector: {value: 'string'}}}}, - unordered_set: {unordered_set: {value: 'string'}}, - user_car_map: {unordered_map: {value: Car}}, - big_num: 'bigint', - date: 'date' - }; - - constructor() { - this.is_inited = false; - this.records = {}; - this.car = new Car(); - this.messages = []; - // account_id -> message - this.efficient_recordes = new UnorderedMap("a"); - // id -> account_id -> message - this.nested_efficient_recordes = new UnorderedMap("b"); - // id -> account_id -> message - this.nested_lookup_recordes = new UnorderedMap("c"); - // index -> account_id -> message - this.vector_nested_group = new Vector("d"); - // account_id -> index -> message - this.lookup_nest_vec = new LookupMap("e"); - this.unordered_set = new UnorderedSet("f"); - this.user_car_map = new UnorderedMap("g"); - this.big_num = 1n; - this.date = new Date(); - } +export class StatusDeserializeClass { + static schema = { + is_inited: "boolean", + records: {map: { key: 'string', value: 'string' }}, + truck: Truck, + messages: {array: {value: 'string'}}, + efficient_recordes: {class: UnorderedMap}, + nested_efficient_recordes: {class: UnorderedMap, value: UnorderedMap}, + nested_lookup_recordes: {class: UnorderedMap, value: {class: LookupMap }}, + vector_nested_group: {class: Vector, value: { class: LookupMap }}, + lookup_nest_vec: { class: LookupMap, value: Vector }, + unordered_set: {class: UnorderedSet }, + user_car_map: {class: UnorderedMap, value: Car }, + big_num: 'bigint', + date: 'date' + }; + constructor() { + this.is_inited = false; + this.records = {}; + this.truck = new Truck(); + this.messages = []; + // account_id -> message + this.efficient_recordes = new UnorderedMap("a"); + // id -> account_id -> message + this.nested_efficient_recordes = new UnorderedMap("b"); + // id -> account_id -> message + this.nested_lookup_recordes = new UnorderedMap("c"); + // index -> account_id -> message + this.vector_nested_group = new Vector("d"); + // account_id -> index -> message + this.lookup_nest_vec = new LookupMap("e"); + this.unordered_set = new UnorderedSet("f"); + this.user_car_map = new UnorderedMap("g"); + this.big_num = 1n; + this.date = new Date(); + this.message_without_schema_defined = ""; + this.number_without_schema_defined = 0; + this.records_without_schema_defined = {}; + } // other methods } ``` @@ -146,12 +149,12 @@ export class Contract { } } ``` -After we set schema info we don't need to set `reconstructor` in `GetOptions`, sdk can infer which reconstructor should be took by the schema: +After we set schema info we don't need to set `reconstructor` in `GetOptions` anymore, sdk can infer which reconstructor should be taken by the schema: ```typescript @NearBindgen({}) export class Contract { static schema = { - outerMap: {unordered_map: {value: { unordered_map: {value: 'string'}}}} + outerMap: {class: UnorderedMap, value: UnorderedMap} }; outerMap: UnorderedMap>; @@ -162,9 +165,7 @@ export class Contract { @view({}) get({id, accountId}: { id: string; accountId: string }) { - const innerMap = this.outerMap.get(id, { - reconstructor: UnorderedMap.reconstruct, // we need to announce reconstructor explicit, reconstructor can be infered from static schema - }); + const innerMap = this.outerMap.get(id); // reconstructor can be infered from static schema if (innerMap === null) { return null; } diff --git a/examples/src/status-deserialize-class.js b/examples/src/status-deserialize-class.js index db8d7fcc..aa76039c 100644 --- a/examples/src/status-deserialize-class.js +++ b/examples/src/status-deserialize-class.js @@ -52,19 +52,12 @@ export class StatusDeserializeClass { records: {map: { key: 'string', value: 'string' }}, truck: Truck, messages: {array: {value: 'string'}}, - // efficient_recordes: {class: {reconstructor: UnorderedMap.reconstruct, value: 'string'}}, efficient_recordes: {class: UnorderedMap}, - // nested_efficient_recordes: {collection: {reconstructor: UnorderedMap.reconstruct, value: { collection: {reconstructor: UnorderedMap.reconstruct, value: 'string'}}}}, nested_efficient_recordes: {class: UnorderedMap, value: UnorderedMap}, - // nested_lookup_recordes: {collection: {reconstructor: UnorderedMap.reconstruct, value: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}}}}, nested_lookup_recordes: {class: UnorderedMap, value: {class: LookupMap }}, - // vector_nested_group: {collection: {reconstructor: Vector.reconstruct, value: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}}}}, vector_nested_group: {class: Vector, value: { class: LookupMap }}, - // lookup_nest_vec: {collection: {reconstructor: LookupMap.reconstruct, value: { collection: { reconstructor: Vector.reconstruct, value: 'string' }}}}, lookup_nest_vec: { class: LookupMap, value: Vector }, - // unordered_set: {collection: {reconstructor: UnorderedSet.reconstruct, value: 'string'}}, unordered_set: {class: UnorderedSet }, - // user_car_map: {collection: {reconstructor: UnorderedMap.reconstruct, value: Car }}, user_car_map: {class: UnorderedMap, value: Car }, big_num: 'bigint', date: 'date' From 08924fbbd812695e500975a48cdadac711c95335 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Mon, 29 Jan 2024 19:38:33 +0100 Subject: [PATCH 4/6] fix: improved text on file --- AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md | 150 +++++++++++++++++----------- 1 file changed, 90 insertions(+), 60 deletions(-) diff --git a/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md b/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md index 791d45fa..df663780 100644 --- a/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md +++ b/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md @@ -1,7 +1,14 @@ -# Auto reconstruct by json schema -## Problem Solved: Could not decode contract state to class instance in early version of sdk -JS SDK decode contract as utf-8 and parse it as JSON, results in a JS Object. -One thing not intuitive is objects are recovered as Object, not class instance. For example, Assume an instance of this class is stored in contract state: +# JSON Schemas for Automatic Decoding of the State + +A limitation that we early detected in the `near-sdk-js` is that Classes and Nested Structures (e.g. Vectors of Maps) are valid to declare as attributes of a contract, but hard to correctly deserialize. + +This file explains a new solution currently implemented in the SDK and how to use it to simplify hanlding stored Classes and Nested Structures. + +## The Problem +NEAR smart contracts store information in their state, which they read when an execution starts and write when an execution finished. In particular, all the information stored in the contract is (de)serialized as a `utf8` `JSON-String`. + +Since Javascript does **not** handle types, it is actually very hard to infer the type of the data that is stored when the contract is loaded at the start of an execution. Imagine for example a contract storing a class `Car` defined as follows: + ```typescript Class Car { name: string; @@ -12,55 +19,67 @@ Class Car { } } ``` -When load it back, the SDK gives us something like: + +A particular instance of that Car (e.g. new Car("Audi", 200)) will be stored in the contract as the JSON string: + ```json {"name": "Audi", "speed": 200} ``` -However this is a JS Object, not an instance of Car Class, and therefore you cannot call run method on it. -This also applies to when user passes a JSON argument to a contract method. If the contract is written in TypeScript, although it may look like: -```typescript -add_a_car(car: Car) { - car.run(); // doesn't work - this.some_collection.set(car.name, car); -} + +Next time the contract is called, the state will be parsed using `JSON.parse()`, and the result will be an `Object {name: "Audi", speed:200}`, which is an instance of `object` and **not an instance of Car**. This would happen both if the user wrote the contract in `javascript` or `typescript`, since casting in `Typescript` is just sugarcoating, it does not actually cast the object! What this means is that: + +```js +// the SDK parses the String into an Object +this.car.run() # This will fail! ``` -But car.run() doesn't work, because SDK only know how to deserialize it as a plain object, not a Car instance. -This problem is particularly painful when class is nested, for example collection class instance LookupMap containing Car class instance. Currently SDK mitigate this problem by requires user to manually reconstruct the JS object to an instance of the original class. -## A method to decode string to class instance by json schema file -we just need to add static member in the class type. -```typescript + +This problem is particularly painful when the class is nested in another Class, e.g. a `LookupMap` of `Cars`. + +## The (non-elegant) Solution +Before, the SDK mitigated this problem by requiring the user to manually reconstruct the JS `Object` to an instance of the original class. + +## A More Elegant Solution: JSON Schemas +To help the SDK know which type it should decode, we can add a `static schema` map, which tells the SDK what type of data it should read: + +```ts Class Car { + // Schema to (de)serialize static schema = { name: "string", speed: "number", }; + + // Properties name: string; speed: number; - + + // methods run() { // ... } } ``` -After we add static member in the class type in our smart contract, it will auto reconstruct smart contract and it's member to class instance recursive by sdk. -And we can call class's functions directly after it deserialized. + +If a `Class` defines an schema, the SDK will recursively reconstruct it, by creating a new instance of `Car` and filling its attributes with the right values. In this way, the deserialized object will effectively be **an instance of the Class**. This means that we can call all its methods: + ```js -add_a_car(car: Car) { - car.run(); // it works! - this.some_collection.set(car.name, car); -} +// the SDK iteratively reconstructs the Car +this.car.run() # This now works! ``` -### The schema format -#### We support multiple type in schema: -* build-in non object types: `string`, `number`, `boolean` -* build-in object types: `Date`, `BigInt`. And we can skip those two build-in object types in schema info -* build-in collection types: `array`, `map` - * for `array` type, we need to declare it in the format of `{array: {value: valueType}}` - * for `map` type, we need to declare it in the format of `{map: {key: 'KeyType', value: 'valueType'}}` -* Custom Class types: `Car` or any class types -* Near collection class types: `Vector`, `LookupMap`, `LookupSet`, `UnorderedMap`, `UnorderedSet`, need to declare in the format of `{class: ClassType, value: ValueType}` - * we can ignore `value` field if we don't need to auto reconstruct value to specific class. we often ignore `value` field if value tye are `string`, `number`, `boolean` -We have a test example which contains all those types in one schema: [status-deserialize-class.js](./examples/src/status-deserialize-class.js) + +## The schema format +The Schema supports multiple types: + +* Primitive types: `string`, `number`, `boolean` +* Built-in object types: `Date`, `BigInt`. +* Built-in collections: `array`, `map` + * Arrays need to be declared as `{array: {value: valueType}}` + * Maps need to declared as `{map: {key: 'keyType', value: 'valueType'}}` +* Custom classes are denoted by their name, e.g. `Car` +* Near SDK Collections (i.e. `Vector`, `LookupMap`, `LookupSet`, `UnorderedMap`, `UnorderedSet`) need to be declared as `{class: ClassType, value: ValueType}` + +You can see a complete example in the [status-deserialize-class](./examples/src/status-deserialize-class.js) file, which containts the following Class declaration: + ```js export class StatusDeserializeClass { static schema = { @@ -78,6 +97,7 @@ export class StatusDeserializeClass { big_num: 'bigint', date: 'date' }; + constructor() { this.is_inited = false; this.records = {}; @@ -104,30 +124,12 @@ export class StatusDeserializeClass { // other methods } ``` -#### Logic of auto reconstruct by json schema -The `_reconstruct` method in [near-bindgen.ts](./packages/near-sdk-js/src/near-bindgen.ts) will check whether there exit a schema in smart contract class, if there exist a static schema info, it will be decoded to class by invoking `decodeObj2class`, or it will fallback to previous behavior: -```typescript - static _reconstruct(classObject: object, plainObject: AnyObject): object { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - if (classObject.constructor.schema === undefined) { - for (const item in classObject) { - const reconstructor = classObject[item].constructor?.reconstruct; - - classObject[item] = reconstructor - ? reconstructor(plainObject[item]) - : plainObject[item]; - } - - return classObject; - } - - return decodeObj2class(classObject, plainObject); - } -``` -#### no need to announce GetOptions.reconstructor in decoding nested collections -In this other hand, after we set schema for the Near collections with nested collections, we don't need to announce `reconstructor` when we need to get and decode a nested collections because the data type info in the schema will tell sdk what the nested data type. -Before we set schema if we need to get a nested collection we need to set `reconstructor` in `GetOptions`: + +--- + +#### What happens with the old `reconstructor`? +Until now, users needed to call a `reconstructor` method in order for **Nested Collections** to be properly decoded: + ```typescript @NearBindgen({}) export class Contract { @@ -149,7 +151,9 @@ export class Contract { } } ``` -After we set schema info we don't need to set `reconstructor` in `GetOptions` anymore, sdk can infer which reconstructor should be taken by the schema: + +With schemas, this is no longer needed, as the SDK can correctly infer how to decode the Nested Collections: + ```typescript @NearBindgen({}) export class Contract { @@ -173,3 +177,29 @@ export class Contract { } } ``` + +--- + +#### How Does the Reconstruction Work? +The `_reconstruct` method in [near-bindgen.ts](./packages/near-sdk-js/src/near-bindgen.ts) will check whether an schema exists in the **contract's class**. If such schema exists, it will try to decode it by invoking `decodeObj2class`: + +```typescript + static _reconstruct(classObject: object, plainObject: AnyObject): object { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (classObject.constructor.schema === undefined) { + for (const item in classObject) { + const reconstructor = classObject[item].constructor?.reconstruct; + + classObject[item] = reconstructor + ? reconstructor(plainObject[item]) + : plainObject[item]; + } + + return classObject; + } + + return decodeObj2class(classObject, plainObject); + } +``` + From ed0bda2aeca649bd953c4b96b4675a6c35bfc232 Mon Sep 17 00:00:00 2001 From: Spring Chiu Date: Sun, 25 Feb 2024 12:40:31 +0800 Subject: [PATCH 5/6] chore: simplify reconstruct schema structure --- AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md | 26 ++++++++---------------- examples/src/status-deserialize-class.js | 14 +++++-------- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md b/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md index df663780..d0d9fe4c 100644 --- a/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md +++ b/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md @@ -70,29 +70,26 @@ this.car.run() # This now works! ## The schema format The Schema supports multiple types: -* Primitive types: `string`, `number`, `boolean` +* Primitive types: `string`, `number`, `boolean`. We can remove schema format of `Primitive types` since is no need to reconstruct them. * Built-in object types: `Date`, `BigInt`. * Built-in collections: `array`, `map` - * Arrays need to be declared as `{array: {value: valueType}}` - * Maps need to declared as `{map: {key: 'keyType', value: 'valueType'}}` + * Arrays need to be declared as `{array: {value: valueType}}`, there are no reconstruct for `Primitive types`, for the value type is `Primitive types`, we can remove this field. + * Maps need to declared as `{map: {key: 'keyType', value: 'valueType'}}`, there are no reconstruct for `Primitive types`, for the key and value type are `Primitive types`, we can remove this field. * Custom classes are denoted by their name, e.g. `Car` -* Near SDK Collections (i.e. `Vector`, `LookupMap`, `LookupSet`, `UnorderedMap`, `UnorderedSet`) need to be declared as `{class: ClassType, value: ValueType}` +* Near SDK Collections (i.e. `Vector`, `LookupMap`, `LookupSet`, `UnorderedMap`, `UnorderedSet`) need to be declared as `{class: ClassType, value: ValueType}` if we need to reconstruct value or we can simplify to mark `ClassType` if we no need to reconstruct value for `Primitive types`. -You can see a complete example in the [status-deserialize-class](./examples/src/status-deserialize-class.js) file, which containts the following Class declaration: +You can see a complete example in the [status-deserialize-class](./examples/src/status-deserialize-class.js) file, which contains the following Class declaration: ```js export class StatusDeserializeClass { static schema = { - is_inited: "boolean", - records: {map: { key: 'string', value: 'string' }}, truck: Truck, - messages: {array: {value: 'string'}}, - efficient_recordes: {class: UnorderedMap}, + efficient_recordes: UnorderedMap, nested_efficient_recordes: {class: UnorderedMap, value: UnorderedMap}, - nested_lookup_recordes: {class: UnorderedMap, value: {class: LookupMap }}, - vector_nested_group: {class: Vector, value: { class: LookupMap }}, + nested_lookup_recordes: {class: UnorderedMap, value: LookupMap}, + vector_nested_group: {class: Vector, value: LookupMap}, lookup_nest_vec: { class: LookupMap, value: Vector }, - unordered_set: {class: UnorderedSet }, + unordered_set: UnorderedSet, user_car_map: {class: UnorderedMap, value: Car }, big_num: 'bigint', date: 'date' @@ -103,15 +100,10 @@ export class StatusDeserializeClass { this.records = {}; this.truck = new Truck(); this.messages = []; - // account_id -> message this.efficient_recordes = new UnorderedMap("a"); - // id -> account_id -> message this.nested_efficient_recordes = new UnorderedMap("b"); - // id -> account_id -> message this.nested_lookup_recordes = new UnorderedMap("c"); - // index -> account_id -> message this.vector_nested_group = new Vector("d"); - // account_id -> index -> message this.lookup_nest_vec = new LookupMap("e"); this.unordered_set = new UnorderedSet("f"); this.user_car_map = new UnorderedMap("g"); diff --git a/examples/src/status-deserialize-class.js b/examples/src/status-deserialize-class.js index aa76039c..e2bf53f7 100644 --- a/examples/src/status-deserialize-class.js +++ b/examples/src/status-deserialize-class.js @@ -28,8 +28,7 @@ class Truck { static schema = { name: "string", speed: "number", - // loads: {collection: {reconstructor: UnorderedMap.reconstruct, value: 'string'}} - loads: {class: UnorderedMap } + loads: UnorderedMap }; constructor() { this.name = ""; @@ -48,16 +47,13 @@ class Truck { @NearBindgen({}) export class StatusDeserializeClass { static schema = { - is_inited: "boolean", - records: {map: { key: 'string', value: 'string' }}, truck: Truck, - messages: {array: {value: 'string'}}, - efficient_recordes: {class: UnorderedMap}, + efficient_recordes: UnorderedMap, nested_efficient_recordes: {class: UnorderedMap, value: UnorderedMap}, - nested_lookup_recordes: {class: UnorderedMap, value: {class: LookupMap }}, - vector_nested_group: {class: Vector, value: { class: LookupMap }}, + nested_lookup_recordes: {class: UnorderedMap, value: LookupMap}, + vector_nested_group: {class: Vector, value: LookupMap}, lookup_nest_vec: { class: LookupMap, value: Vector }, - unordered_set: {class: UnorderedSet }, + unordered_set: UnorderedSet, user_car_map: {class: UnorderedMap, value: Car }, big_num: 'bigint', date: 'date' From 0fc7cbe226c19fb30226ef34290a3df48e37316a Mon Sep 17 00:00:00 2001 From: Bo Yao Date: Mon, 26 Feb 2024 09:56:58 +0800 Subject: [PATCH 6/6] Update AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md --- AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md b/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md index d0d9fe4c..0915c322 100644 --- a/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md +++ b/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md @@ -2,7 +2,7 @@ A limitation that we early detected in the `near-sdk-js` is that Classes and Nested Structures (e.g. Vectors of Maps) are valid to declare as attributes of a contract, but hard to correctly deserialize. -This file explains a new solution currently implemented in the SDK and how to use it to simplify hanlding stored Classes and Nested Structures. +This doc explains a new solution currently implemented in the SDK and how to use it to simplify hanlding stored Classes and Nested Structures. ## The Problem NEAR smart contracts store information in their state, which they read when an execution starts and write when an execution finished. In particular, all the information stored in the contract is (de)serialized as a `utf8` `JSON-String`.