Skip to content

Commit

Permalink
Add schema validator
Browse files Browse the repository at this point in the history
  • Loading branch information
pouya-eghbali committed May 28, 2023
1 parent 2e382f8 commit 1190d7d
Show file tree
Hide file tree
Showing 17 changed files with 257 additions and 142 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ solidq schema.yaml MyContract.sol

```yaml
Book:
Id: uint256 id auto
Name: string
Author: string indexed
Published: uint256 indexed get set
Expand All @@ -64,6 +65,10 @@ is equivalent to
```yaml
Book:
Id:
type: uint256
id: true
auto: true
Name: string
Author:
type: string
Expand All @@ -84,15 +89,18 @@ User:
Address:
type: address
id: true
Balance:
type: uint256
```
You can learn the schema language [here](./schema.md).
![SolidQuery Demo](./assets/solidq.demo.png)
## Roadmap
We have planned the following features:
- Schema validation
- Fine control over contract generation
- Array fields
- Relations
Expand Down
7 changes: 7 additions & 0 deletions lib/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import camelCase from "camelcase";
*/
export const toCamel = (...arr) => camelCase(arr.join("-"));

const validProperties = ["id", "auto", "indexed", "get", "set"];

/**
* Parses a field type into an object with the type and whether it's indexed.
* @param {(string|Object)} type - The field type to be parsed. If a string, it
Expand All @@ -17,6 +19,11 @@ export const toCamel = (...arr) => camelCase(arr.join("-"));
export const parseFieldType = (fieldType) => {
if (typeof fieldType === "string") {
const [type, ...properties] = fieldType.split(" ");
for (const property of properties) {
if (!validProperties.includes(property)) {
throw new Error(`"${property}" is not a valid modifier`);
}
}
return {
type,
...Object.fromEntries(properties.map((property) => [property, true])),
Expand Down
17 changes: 6 additions & 11 deletions lib/crud/create.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { toCamel, parseFieldType } from "../common.js";
import { getIdField } from "../helpers/id.js";
import { toCamel } from "../common.js";

/**
* Generates Solidity functions for adding records to the schema's structures.
Expand All @@ -9,11 +8,9 @@ import { getIdField } from "../helpers/id.js";
*/
export const getAddFunctions = (schema) => {
const functions = [];
for (const [structName, details] of Object.entries(schema)) {
const id = getIdField(structName, details);
for (const [structName, { id, fields }] of Object.entries(schema)) {
const indexes = [];
for (const [fieldName, fieldType] of Object.entries(details)) {
const { indexed } = parseFieldType(fieldType);
for (const [fieldName, { indexed }] of Object.entries(fields)) {
if (indexed) {
const indexFunctionName = toCamel(
"add",
Expand All @@ -27,16 +24,14 @@ export const getAddFunctions = (schema) => {
}
}
const functionName = toCamel("add", structName);
const fields = Object.keys(details)
.filter((field) => field !== id.name)
.map((field) => `value.${field}`);
const eventFields = Object.keys(fields).map((field) => `value.${field}`);
const counterName = toCamel(structName, "counter");
if (id.auto) {
const body = [
`uint256 ${id.name} = ${counterName}++;`,
`${structName}s[${id.name}] = value;`,
`${indexes.join("\n")}`,
`emit ${structName}Created(${id.name}, ${fields.join(", ")});`,
`emit ${structName}Created(${id.name}, ${eventFields.join(", ")});`,
`return ${id.name};`,
]
.filter(Boolean)
Expand All @@ -58,7 +53,7 @@ export const getAddFunctions = (schema) => {
const body = [
`${structName}s[${idName}] = value;`,
`${indexes.join("\n")}`,
`emit ${structName}Created(${idName}, ${fields.join(", ")});`,
`emit ${structName}Created(${idName}, ${eventFields.join(", ")});`,
]
.filter(Boolean)
.join("\n");
Expand Down
9 changes: 3 additions & 6 deletions lib/crud/delete.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { toCamel, parseFieldType } from "../common.js";
import { getIdField } from "../helpers/id.js";
import { toCamel } from "../common.js";

/**
* Generates Solidity functions for deleting records from the schema's
Expand All @@ -10,11 +9,9 @@ import { getIdField } from "../helpers/id.js";
*/
export const getDeleteFunctions = (schema) => {
const functions = [];
for (const [structName, details] of Object.entries(schema)) {
const id = getIdField(structName, details);
for (const [structName, { id, fields }] of Object.entries(schema)) {
const indexes = [];
for (const [fieldName, fieldType] of Object.entries(details)) {
const { indexed } = parseFieldType(fieldType);
for (const [fieldName, { indexed }] of Object.entries(fields)) {
if (indexed) {
const indexFunctionName = toCamel(
"delete",
Expand Down
18 changes: 6 additions & 12 deletions lib/crud/read.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { toCamel, toCallData, toMemory, parseFieldType } from "../common.js";
import { getIdField } from "../helpers/id.js";
import { toCamel, toCallData, toMemory } from "../common.js";

/**
* Generates Solidity functions for finding records in the schema's
Expand All @@ -10,10 +9,8 @@ import { getIdField } from "../helpers/id.js";
*/
export const getFindFunctions = (schema) => {
const functions = [];
for (const [structName, details] of Object.entries(schema)) {
const id = getIdField(structName, details);
for (const [fieldName, fieldType] of Object.entries(details)) {
const { type, indexed } = parseFieldType(fieldType);
for (const [structName, { id, fields }] of Object.entries(schema)) {
for (const [fieldName, { type, indexed }] of Object.entries(fields)) {
if (indexed) {
const functionName = toCamel("find", structName, "by", fieldName);
const indexName = toCamel(structName, fieldName);
Expand Down Expand Up @@ -43,8 +40,7 @@ export const getFindFunctions = (schema) => {
*/
export const getGetFunctions = (schema) => {
const functions = [];
for (const [structName, details] of Object.entries(schema)) {
const id = getIdField(structName, details);
for (const [structName, { id }] of Object.entries(schema)) {
const functionName = toCamel("get", `${structName}s`, "by", id.name);
functions.push(`
/**
Expand Down Expand Up @@ -74,10 +70,8 @@ export const getGetFunctions = (schema) => {
*/
export const getFieldGetFunctions = (schema) => {
const functions = [];
for (const [structName, details] of Object.entries(schema)) {
const id = getIdField(structName, details);
for (const [fieldName, fieldType] of Object.entries(details)) {
const { type, get } = parseFieldType(fieldType);
for (const [structName, { id, fields }] of Object.entries(schema)) {
for (const [fieldName, { type, get }] of Object.entries(fields)) {
if (get) {
const functionName = toCamel(
"get",
Expand Down
29 changes: 11 additions & 18 deletions lib/crud/update.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { toCamel, parseFieldType, toCallData } from "../common.js";
import { getIdField } from "../helpers/id.js";
import { toCamel, toCallData } from "../common.js";

/**
* Generates Solidity functions for updating records in the schema's structures.
Expand All @@ -9,11 +8,9 @@ import { getIdField } from "../helpers/id.js";
*/
export const getUpdateFunctions = (schema) => {
const functions = [];
for (const [structName, details] of Object.entries(schema)) {
const id = getIdField(structName, details);
for (const [structName, { id, fields }] of Object.entries(schema)) {
const indexes = [];
for (const [fieldName, fieldType] of Object.entries(details)) {
const { indexed } = parseFieldType(fieldType);
for (const [fieldName, { indexed }] of Object.entries(fields)) {
if (indexed) {
const deleteIndexFunction = toCamel(
"delete",
Expand All @@ -38,15 +35,13 @@ export const getUpdateFunctions = (schema) => {
}
}
const functionName = toCamel("update", structName);
const fields = Object.keys(details)
.filter((field) => field !== id.name)
.map((field) => `value.${field}`);
const eventFields = Object.keys(fields).map((field) => `value.${field}`);
const idName = id.name;
const idType = id.type;
const body = [
`${indexes.join("\n")}`,
`${structName}s[${idName}] = value;`,
`emit ${structName}Updated(${idName}, ${fields.join(", ")});`,
`emit ${structName}Updated(${idName}, ${eventFields.join(", ")});`,
]
.filter(Boolean)
.join("\n");
Expand Down Expand Up @@ -74,10 +69,8 @@ export const getUpdateFunctions = (schema) => {
*/
export const getFieldSetFunctions = (schema) => {
const functions = [];
for (const [structName, details] of Object.entries(schema)) {
const id = getIdField(structName, details);
for (const [fieldName, fieldType] of Object.entries(details)) {
const { type, get, indexed } = parseFieldType(fieldType);
for (const [structName, { id, fields }] of Object.entries(schema)) {
for (const [fieldName, { type, get, indexed }] of Object.entries(fields)) {
if (get) {
const functionName = toCamel(
"set",
Expand All @@ -86,9 +79,9 @@ export const getFieldSetFunctions = (schema) => {
"by",
id.name
);
const fields = Object.keys(details)
.filter((field) => field !== id.name)
.map((field) => `${structName}s[${id.name}].${field}`);
const eventFields = Object.keys(fields).map(
(field) => `${structName}s[${id.name}].${field}`
);
const valueType = toCallData(type);
const indexes = [];
if (indexed) {
Expand Down Expand Up @@ -118,7 +111,7 @@ export const getFieldSetFunctions = (schema) => {
const body = [
`${structName}s[${idName}].${fieldName} = value;`,
`${indexes.join("\n")}`,
`emit ${structName}Updated(${idName}, ${fields.join(", ")});`,
`emit ${structName}Updated(${idName}, ${eventFields.join(", ")});`,
]
.filter(Boolean)
.join("\n");
Expand Down
33 changes: 18 additions & 15 deletions lib/generator.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { format } from "prettier";
import parserSolidity from "prettier-plugin-solidity";

import { parseSchema } from "./schema.js";

import {
getStructs,
getStorages,
Expand Down Expand Up @@ -31,24 +33,25 @@ import {
* @return {string} Formatted Solidity contract.
*/
export const generate = (schema) => {
const parsed = parseSchema(schema);
// Data layer
const counters = getCounters(schema);
const structs = getStructs(schema);
const events = getEvents(schema);
const storages = getStorages(schema);
const indexes = getIndexes(schema);
const counters = getCounters(parsed);
const structs = getStructs(parsed);
const events = getEvents(parsed);
const storages = getStorages(parsed);
const indexes = getIndexes(parsed);
// CRUD helpers
const popFunctions = getPopFunctions(schema);
const deleteIndexFunctions = getDeleteIndexFunctions(schema);
const addIndexFunctions = getAddIndexFunctions(schema);
const popFunctions = getPopFunctions(parsed);
const deleteIndexFunctions = getDeleteIndexFunctions(parsed);
const addIndexFunctions = getAddIndexFunctions(parsed);
// CRUD functions
const addFunctions = getAddFunctions(schema);
const deleteFunctions = getDeleteFunctions(schema);
const updateFunctions = getUpdateFunctions(schema);
const setFieldFunctions = getFieldSetFunctions(schema);
const findFunctions = getFindFunctions(schema);
const getFunctions = getGetFunctions(schema);
const getFieldFunctions = getFieldGetFunctions(schema);
const addFunctions = getAddFunctions(parsed);
const deleteFunctions = getDeleteFunctions(parsed);
const updateFunctions = getUpdateFunctions(parsed);
const setFieldFunctions = getFieldSetFunctions(parsed);
const findFunctions = getFindFunctions(parsed);
const getFunctions = getGetFunctions(parsed);
const getFieldFunctions = getFieldGetFunctions(parsed);

const contract = `\
//SPDX-License-Identifier: UNLICENSED
Expand Down
18 changes: 10 additions & 8 deletions lib/helpers/id.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,29 @@ import { parseFieldType } from "../common.js";

/**
* Get the Solidity type and name to use for the IDs of a record type.
* @param {Object} name - The name of the record.
* @param {Object} recordSchema - The schema of the record.
* @return {string} Solidity type for record IDs.
*/
export const getIdField = (name, recordSchema) => {
export const getIdField = (recordSchema) => {
const idTypes = [];
for (const [name, fieldType] of Object.entries(recordSchema)) {
const { type, id, set, get, indexed } = parseFieldType(fieldType);
const { type, id, set, get, indexed, auto } = parseFieldType(fieldType);
if (id) {
if (set) {
error(`${name}:: ID field can't have the "set" modifier`);
throw new Error(`ID field can't have the "set" modifier`);
} else if (get) {
error(`${name}:: ID field can't have the "get" modifier`);
throw new Error(`ID field can't have the "get" modifier`);
} else if (indexed) {
error(`${name}:: ID field can't have the "indexed" modifier`);
throw new Error(`ID field can't have the "indexed" modifier`);
}
idTypes.push({ type, name });
if (auto && !type.startsWith("uint")) {
throw new Error(`The "auto" modifier is only valid for uint* IDs`);
}
idTypes.push({ type, name, auto });
}
}
if (idTypes.length > 1) {
error(`${name}:: Cannot define more than one ID field`);
throw new Error("More than one ID field is defined");
}
return idTypes[0] || { type: "uint256", name: "Id", auto: true };
};
15 changes: 5 additions & 10 deletions lib/helpers/indexes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { toCamel, parseFieldType } from "../common.js";
import { getIdField } from "./id.js";
import { toCamel } from "../common.js";

/**
* Generates Solidity functions for deleting indexed fields from the schema.
Expand All @@ -9,10 +8,8 @@ import { getIdField } from "./id.js";
*/
export const getDeleteIndexFunctions = (schema) => {
const functions = [];
for (const [structName, details] of Object.entries(schema)) {
const id = getIdField(structName, details);
for (const [fieldName, fieldType] of Object.entries(details)) {
const { indexed } = parseFieldType(fieldType);
for (const [structName, { id, fields }] of Object.entries(schema)) {
for (const [fieldName, { indexed }] of Object.entries(fields)) {
if (indexed) {
const indexName = toCamel(structName, fieldName);
const functionName = toCamel(
Expand Down Expand Up @@ -47,10 +44,8 @@ export const getDeleteIndexFunctions = (schema) => {
*/
export const getAddIndexFunctions = (schema) => {
const functions = [];
for (const [structName, details] of Object.entries(schema)) {
const id = getIdField(structName, details);
for (const [fieldName, fieldType] of Object.entries(details)) {
const { indexed } = parseFieldType(fieldType);
for (const [structName, { id, fields }] of Object.entries(schema)) {
for (const [fieldName, { indexed }] of Object.entries(fields)) {
if (indexed) {
const indexName = toCamel(structName, fieldName);
const functionName = toCamel(
Expand Down
Loading

0 comments on commit 1190d7d

Please sign in to comment.