diff --git a/schemas/core/SystemSettings.json b/schemas/core/SystemSettings.json index 730fad42..de638d9d 100644 --- a/schemas/core/SystemSettings.json +++ b/schemas/core/SystemSettings.json @@ -71,7 +71,7 @@ "fieldname": "hideGetStarted", "label": "Hide Get Started", "fieldtype": "Check", - "default": 0, + "default": false, "description": "Hides the Get Started section from the sidebar. Change will be visible on restart or refreshing the app." } ], diff --git a/schemas/helpers.ts b/schemas/helpers.ts new file mode 100644 index 00000000..65e7a570 --- /dev/null +++ b/schemas/helpers.ts @@ -0,0 +1,19 @@ +export function getMapFromList( + list: T[], + name: string = 'name' +): Record { + const acc: Record = {}; + for (const t of list) { + const key = t[name] as string | undefined; + if (key === undefined) { + continue; + } + + acc[key] = t; + } + return acc; +} + +export function getListFromMap(map: Record): T[] { + return Object.keys(map).map((n) => map[n]); +} diff --git a/schemas/index.ts b/schemas/index.ts new file mode 100644 index 00000000..17291fe9 --- /dev/null +++ b/schemas/index.ts @@ -0,0 +1,125 @@ +import { cloneDeep } from 'lodash'; +import { getListFromMap, getMapFromList } from './helpers'; +import regional from './regional'; +import { appSchemas, coreSchemas } from './schemas'; +import { Schema, SchemaMap, SchemaStub, SchemaStubMap } from './types'; + +export function getSchemas(countryCode: string = '-'): SchemaMap { + const builtCoreSchemas = getCoreSchemas(); + const builtAppSchemas = getAppSchemas(countryCode); + + return Object.assign({}, builtAppSchemas, builtCoreSchemas); +} + +function getCoreSchemas(): SchemaMap { + const rawSchemaMap = getMapFromList(coreSchemas); + const coreSchemaMap = getAbstractCombinedSchemas(rawSchemaMap); + return cleanSchemas(coreSchemaMap); +} + +function getAppSchemas(countryCode: string): SchemaMap { + const combinedSchemas = getRegionalCombinedSchemas(countryCode); + const schemaMap = getAbstractCombinedSchemas(combinedSchemas); + return cleanSchemas(schemaMap); +} + +function cleanSchemas(schemaMap: SchemaMap): SchemaMap { + for (const name in schemaMap) { + const schema = schemaMap[name]; + if (schema.isAbstract && !schema.extends) { + delete schemaMap[name]; + continue; + } + + delete schema.extends; + delete schema.isAbstract; + } + + return schemaMap; +} + +function getCombined( + extendingSchema: SchemaStub, + abstractSchema: SchemaStub +): SchemaStub { + abstractSchema = cloneDeep(abstractSchema); + extendingSchema = cloneDeep(extendingSchema); + + const abstractFields = getMapFromList( + abstractSchema.fields ?? [], + 'fieldname' + ); + const extendingFields = getMapFromList( + extendingSchema.fields ?? [], + 'fieldname' + ); + + const combined = Object.assign(abstractSchema, extendingSchema); + + for (const fieldname in extendingFields) { + abstractFields[fieldname] = extendingFields[fieldname]; + } + + combined.fields = getListFromMap(abstractFields); + return combined; +} + +function getAbstractCombinedSchemas(schemas: SchemaStubMap): SchemaMap { + const abstractSchemaNames: string[] = Object.keys(schemas).filter( + (n) => schemas[n].isAbstract + ); + + const extendingSchemaNames: string[] = Object.keys(schemas).filter((n) => + abstractSchemaNames.includes(schemas[n].extends) + ); + + const completeSchemas: Schema[] = Object.keys(schemas) + .filter( + (n) => + !abstractSchemaNames.includes(n) && !extendingSchemaNames.includes(n) + ) + .map((n) => schemas[n] as Schema); + + const schemaMap = getMapFromList(completeSchemas) as SchemaMap; + + for (const name of extendingSchemaNames) { + const extendingSchema = schemas[name] as Schema; + const abstractSchema = schemas[extendingSchema.extends] as SchemaStub; + + schemaMap[name] = getCombined(extendingSchema, abstractSchema) as Schema; + } + + for (const name in abstractSchemaNames) { + delete schemaMap[name]; + } + + return schemaMap; +} + +function getRegionalCombinedSchemas(countryCode: string): SchemaStubMap { + const regionalSchemaMap = getRegionalSchema(countryCode); + const appSchemaMap = getMapFromList(appSchemas); + const combined = { ...appSchemaMap }; + + for (const name in regionalSchemaMap) { + const regionalSchema = regionalSchemaMap[name]; + + if (!combined.hasOwnProperty(name)) { + combined[name] = regionalSchema; + continue; + } + + combined[name] = getCombined(regionalSchema, combined[name]); + } + + return combined; +} + +function getRegionalSchema(countryCode: string): SchemaStubMap { + const regionalSchemas = regional[countryCode] as SchemaStub[] | undefined; + if (regionalSchemas === undefined) { + return {}; + } + + return getMapFromList(regionalSchemas); +} diff --git a/schemas/regional/in/AccountingSettings.json b/schemas/regional/in/AccountingSettings.json index 32391b55..66de67ac 100644 --- a/schemas/regional/in/AccountingSettings.json +++ b/schemas/regional/in/AccountingSettings.json @@ -8,7 +8,6 @@ "placeholder": "27AAAAA0000A1Z5" } ], - "keywordFields": [], "quickEditFields": [ "fullname", "email", diff --git a/schemas/regional/in/Party.json b/schemas/regional/in/Party.json index 82bd30f2..7076ab27 100644 --- a/schemas/regional/in/Party.json +++ b/schemas/regional/in/Party.json @@ -12,7 +12,20 @@ "placeholder": "GST Registration", "fieldtype": "Select", "default": "Unregistered", - "options": ["Unregistered", "Registered Regular", "Consumer"] + "options": [ + { + "value": "Unregistered", + "label": "Unregistered" + }, + { + "value": "Registered Regular", + "label": "Registered Regular" + }, + { + "value": "Consumer", + "label": "Consumer" + } + ] } ], "quickEditFields": [ diff --git a/schemas/regional/in/index.ts b/schemas/regional/in/index.ts new file mode 100644 index 00000000..835ca90a --- /dev/null +++ b/schemas/regional/in/index.ts @@ -0,0 +1,6 @@ +import { SchemaStub } from '../../types'; +import AccountingSettings from './AccountingSettings.json'; +import Address from './Address.json'; +import Party from './Party.json'; + +export default [AccountingSettings, Address, Party] as SchemaStub[]; diff --git a/schemas/regional/index.ts b/schemas/regional/index.ts new file mode 100644 index 00000000..3616cca1 --- /dev/null +++ b/schemas/regional/index.ts @@ -0,0 +1,6 @@ +import IndianSchemas from './in'; + +/** + * Regional Schemas are exported by country code. + */ +export default { in: IndianSchemas }; diff --git a/schemas/schemas.ts b/schemas/schemas.ts index 8eefb8c7..171f1265 100644 --- a/schemas/schemas.ts +++ b/schemas/schemas.ts @@ -28,7 +28,7 @@ import TaxSummary from './app/TaxSummary.json'; import PatchRun from './core/PatchRun.json'; import SingleValue from './core/SingleValue.json'; import SystemSettings from './core/SystemSettings.json'; -import { Schema } from './types'; +import { Schema, SchemaStub } from './types'; export const coreSchemas: Schema[] = [ PatchRun as Schema, @@ -36,7 +36,7 @@ export const coreSchemas: Schema[] = [ SystemSettings as Schema, ]; -export const appSchemas: Schema[] = [ +export const appSchemas: Schema[] | SchemaStub[] = [ SetupWizard as Schema, GetStarted as Schema, @@ -52,8 +52,8 @@ export const appSchemas: Schema[] = [ AccountingLedgerEntry as Schema, Party as Schema, - Supplier as Schema, - Customer as Schema, + Supplier as SchemaStub, + Customer as SchemaStub, Address as Schema, Item as Schema, diff --git a/schemas/types.ts b/schemas/types.ts index 4ba11b1f..44095baa 100644 --- a/schemas/types.ts +++ b/schemas/types.ts @@ -1,3 +1,38 @@ +/** + * # Schema + * + * Main purpose of this is to describe the shape of the Models table in the + * database. But there is some irrelevant information in the schemas with + * respect to this goal. This is information is allowed as long as it is not + * dynamic, which is impossible anyways as the files are data (.json !.js) + * + * If any field has to have a dynamic value, it should be added to the controller + * file by the same name. + * + * + * There are a few types of schemas: + * - _Regional_: Schemas that are in the '../regional' subdirectories + * these can be of any of the below types. + * - _Abstract_: Schemas that are not used as they are but only after they are + * extended by Stub schemas. Indentified by the `isAbstract` field + * - _Subclass_: Schemas that have an "extends" field on them, the value of which + * points to an Abstract schema. + * - _Complete_: Schemas which are neither abstract nor stub. + * + * + * ## Final Schema + * + * This is the schema which is used by the database and app code. + * + * The order in which a schema is built is: + * 1. Build _Regional_ schemas by overriding the fields and other properties of the + * non regional variants. + * 2. Combine _Subclass_ schemas with _Abstract_ schemas to get complete schemas. + * + * Note: if a Regional schema is not present as a non regional variant it's used + * as it is. + */ + export enum FieldTypeEnum { Data = 'Data', Select = 'Select', @@ -19,18 +54,19 @@ export enum FieldTypeEnum { export type FieldType = keyof typeof FieldTypeEnum; export type RawValue = string | number | boolean; +// prettier-ignore export interface BaseField { - fieldname: string; - fieldtype: FieldType; - label: string; - hidden?: boolean; - required?: boolean; - readOnly?: boolean; - description?: string; - default?: RawValue; - placeholder?: string; - groupBy?: string; - computed?: boolean; + fieldname: string; // Column name in the db + fieldtype: FieldType; // UI Descriptive field types that map to column types + label: string; // Translateable UI facing name + required?: boolean; // Implies Not Null + hidden?: boolean; // UI Facing config, whether field is shown in a form + readOnly?: boolean; // UI Facing config, whether field is editable + description?: string; // UI Facing, translateable, used for inline documentation + default?: RawValue; // Default value of a field, should match the db type + placeholder?: string; // UI Facing config, form field placeholder + groupBy?: string; // UI Facing used in dropdowns fields + computed?: boolean; // Indicates whether a value is computed, implies readonly } export type SelectOption = { value: string; label: string }; @@ -42,20 +78,23 @@ export interface OptionField extends BaseField { options: SelectOption[]; } +// prettier-ignore export interface TargetField extends BaseField { fieldtype: FieldTypeEnum.Table | FieldTypeEnum.Link; - target: string | string[]; + target: string | string[]; // Name of the table or group of tables to fetch values } +// prettier-ignore export interface DynamicLinkField extends BaseField { fieldtype: FieldTypeEnum.DynamicLink; - references: string; + references: string; // Reference to an option field that links to schema } +// prettier-ignore export interface NumberField extends BaseField { fieldtype: FieldTypeEnum.Float | FieldTypeEnum.Int; - minvalue?: number; - maxvalue?: number; + minvalue?: number; // UI Facing used to restrict lower bound + maxvalue?: number; // UI Facing used to restrict upper bound } export type Field = @@ -66,16 +105,24 @@ export type Field = | NumberField; export type TreeSettings = { parentField: string }; + +// prettier-ignore export interface Schema { - name: string; - label: string; - fields: Field[]; - isTree?: boolean; - extends?: string; - isChild?: boolean; - isSingle?: boolean; - isAbstract?: boolean; - isSubmittable?: boolean; - keywordFields?: string[]; - treeSettings?: TreeSettings; + name: string; // Table PK + label: string; // Translateable UI facing name + fields: Field[]; // Maps to database columns + isTree?: boolean; // Used for nested set, eg for Chart of Accounts + extends?: string; // Value points to an Abstract schema. Indicates Subclass schema + isChild?: boolean; // Indicates a child table, i.e table with "parent" FK column + isSingle?: boolean; // Fields will be values in SingleValue, i.e. an Entity Attr. Value + isAbstract?: boolean; // Not entered into db, used to extend a Subclass schema + isSubmittable?: boolean; // For transactional types, values considered only after submit + keywordFields?: string[]; // Used for fields that are to be used for search. + treeSettings?: TreeSettings; // Used to determine root nodes } + +export interface SchemaStub extends Partial { + name: string; +} +export type SchemaMap = Record; +export type SchemaStubMap = Record; diff --git a/src/main.js b/src/main.js index 1ddf31b2..a0264169 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,7 @@ import { ipcRenderer } from 'electron'; import frappe from 'frappe'; import { createApp } from 'vue'; +import { getSchemas } from '../schemas'; import App from './App'; import FeatherIcon from './components/FeatherIcon'; import config, { ConfigKeys } from './config'; @@ -101,3 +102,5 @@ import { setLanguageMap, stringifyCircular } from './utils'; handleError(true, error, {}, () => process.exit(1)); }); })(); + +window.gs = getSchemas;