2
0
mirror of https://github.com/frappe/books.git synced 2024-12-22 19:09:01 +00:00

incr: build schemas, add some notes

This commit is contained in:
18alantom 2022-03-23 20:16:19 +05:30
parent 879b677b3b
commit ef6e8d44ab
10 changed files with 251 additions and 33 deletions

View File

@ -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."
}
],

19
schemas/helpers.ts Normal file
View File

@ -0,0 +1,19 @@
export function getMapFromList<T>(
list: T[],
name: string = 'name'
): Record<string, T> {
const acc: Record<string, T> = {};
for (const t of list) {
const key = t[name] as string | undefined;
if (key === undefined) {
continue;
}
acc[key] = t;
}
return acc;
}
export function getListFromMap<T>(map: Record<string, T>): T[] {
return Object.keys(map).map((n) => map[n]);
}

125
schemas/index.ts Normal file
View File

@ -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);
}

View File

@ -8,7 +8,6 @@
"placeholder": "27AAAAA0000A1Z5"
}
],
"keywordFields": [],
"quickEditFields": [
"fullname",
"email",

View File

@ -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": [

View File

@ -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[];

View File

@ -0,0 +1,6 @@
import IndianSchemas from './in';
/**
* Regional Schemas are exported by country code.
*/
export default { in: IndianSchemas };

View File

@ -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,

View File

@ -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<Schema> {
name: string;
}
export type SchemaMap = Record<string, Schema>;
export type SchemaStubMap = Record<string, SchemaStub>;

View File

@ -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;