@astroapps/forms-core 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,318 @@
1
+ import {
2
+ CompoundField,
3
+ FieldOption,
4
+ FieldType,
5
+ isCompoundField,
6
+ SchemaField,
7
+ SchemaMap,
8
+ } from "./schemaField";
9
+
10
+ export type AllowedSchema<T> = T extends string
11
+ ? SchemaField & {
12
+ type:
13
+ | FieldType.String
14
+ | FieldType.Date
15
+ | FieldType.DateTime
16
+ | FieldType.Time;
17
+ }
18
+ : T extends number
19
+ ? SchemaField & {
20
+ type: FieldType.Int | FieldType.Double;
21
+ }
22
+ : T extends boolean
23
+ ? SchemaField & {
24
+ type: FieldType.Bool;
25
+ }
26
+ : T extends Array<infer E>
27
+ ? AllowedSchema<E> & {
28
+ collection: true;
29
+ }
30
+ : T extends { [key: string]: any }
31
+ ? CompoundField & {
32
+ type: FieldType.Compound;
33
+ }
34
+ : SchemaField & { type: FieldType.Any };
35
+
36
+ type AllowedField<T, K> = (
37
+ name: string,
38
+ ) => (SchemaField & { type: K }) | AllowedSchema<T>;
39
+
40
+ export function buildSchema<T, Custom = "">(def: {
41
+ [K in keyof T]-?: AllowedField<T[K], Custom>;
42
+ }): SchemaField[] {
43
+ return Object.entries(def).map((x) =>
44
+ (x[1] as (n: string) => SchemaField)(x[0]),
45
+ );
46
+ }
47
+
48
+ export type FieldBuilder<T extends FieldType, K> = (
49
+ name: string,
50
+ ) => Omit<SchemaField, "type"> & { type: T } & K;
51
+
52
+ export function stringField(
53
+ displayName: string,
54
+ ): FieldBuilder<FieldType.String, {}>;
55
+
56
+ export function stringField<S extends Partial<SchemaField>>(
57
+ displayName: string,
58
+ options: S,
59
+ ): FieldBuilder<FieldType.String, S>;
60
+
61
+ export function stringField(displayName: string, options?: any) {
62
+ return makeScalarField({
63
+ type: FieldType.String,
64
+ displayName,
65
+ ...options,
66
+ });
67
+ }
68
+
69
+ export function stringOptionsField(
70
+ displayName: string,
71
+ ...options: FieldOption[]
72
+ ) {
73
+ return makeScalarField({
74
+ type: FieldType.String as const,
75
+ displayName,
76
+ options,
77
+ });
78
+ }
79
+
80
+ export function withScalarOptions<
81
+ S extends SchemaField,
82
+ S2 extends Partial<SchemaField>,
83
+ >(options: S2, v: (name: string) => S): (name: string) => S & S2 {
84
+ return (n) => ({ ...v(n), ...options });
85
+ }
86
+
87
+ export function makeScalarField<S extends Partial<SchemaField>>(
88
+ options: S,
89
+ ): (name: string) => SchemaField & S {
90
+ return (n) => ({ ...defaultScalarField(n, n), ...options });
91
+ }
92
+
93
+ export function makeCompoundField<S extends Partial<CompoundField>>(
94
+ options: S,
95
+ ): (name: string) => CompoundField & {
96
+ type: FieldType.Compound;
97
+ } & S {
98
+ return (n) => ({ ...defaultCompoundField(n, n, false), ...options });
99
+ }
100
+
101
+ export function intField(displayName: string): FieldBuilder<FieldType.Int, {}>;
102
+
103
+ export function intField<S extends Partial<SchemaField>>(
104
+ displayName: string,
105
+ options: S,
106
+ ): FieldBuilder<FieldType.Int, S>;
107
+
108
+ export function intField<S extends Partial<SchemaField>>(
109
+ displayName: string,
110
+ options?: S,
111
+ ) {
112
+ return makeScalarField({
113
+ type: FieldType.Int as const,
114
+ displayName,
115
+ ...(options as S),
116
+ });
117
+ }
118
+
119
+ export function doubleField(
120
+ displayName: string,
121
+ ): FieldBuilder<FieldType.Double, {}>;
122
+
123
+ export function doubleField<S extends Partial<SchemaField>>(
124
+ displayName: string,
125
+ options: S,
126
+ ): FieldBuilder<FieldType.Double, S>;
127
+
128
+ export function doubleField<S extends Partial<SchemaField>>(
129
+ displayName: string,
130
+ options?: S,
131
+ ) {
132
+ return makeScalarField({
133
+ type: FieldType.Double as const,
134
+ displayName,
135
+ ...(options as S),
136
+ });
137
+ }
138
+
139
+ export function dateField(
140
+ displayName: string,
141
+ ): FieldBuilder<FieldType.Date, {}>;
142
+
143
+ export function dateField<S extends Partial<SchemaField>>(
144
+ displayName: string,
145
+ options: S,
146
+ ): FieldBuilder<FieldType.Date, S>;
147
+
148
+ export function dateField<S extends Partial<SchemaField>>(
149
+ displayName: string,
150
+ options?: S,
151
+ ) {
152
+ return makeScalarField({
153
+ type: FieldType.Date as const,
154
+ displayName,
155
+ ...(options as S),
156
+ });
157
+ }
158
+
159
+ export function timeField<S extends Partial<SchemaField>>(
160
+ displayName: string,
161
+ options?: S,
162
+ ) {
163
+ return makeScalarField({
164
+ type: FieldType.Time as const,
165
+ displayName,
166
+ ...(options as S),
167
+ });
168
+ }
169
+
170
+ export function dateTimeField<S extends Partial<SchemaField>>(
171
+ displayName: string,
172
+ options?: S,
173
+ ) {
174
+ return makeScalarField({
175
+ type: FieldType.DateTime as const,
176
+ displayName,
177
+ ...(options as S),
178
+ });
179
+ }
180
+
181
+ export function boolField(
182
+ displayName: string,
183
+ ): FieldBuilder<FieldType.Bool, {}>;
184
+
185
+ export function boolField<S extends Partial<SchemaField>>(
186
+ displayName: string,
187
+ options: S,
188
+ ): FieldBuilder<FieldType.Bool, S>;
189
+
190
+ export function boolField(displayName: string, options?: any) {
191
+ return makeScalarField({
192
+ type: FieldType.Bool as const,
193
+ displayName,
194
+ ...options,
195
+ });
196
+ }
197
+
198
+ export function compoundField<
199
+ Other extends Partial<Omit<CompoundField, "type" | "schemaType">>,
200
+ >(
201
+ displayName: string,
202
+ fields: SchemaField[],
203
+ other?: Other,
204
+ ): (name: string) => CompoundField & {
205
+ collection: Other["collection"];
206
+ } {
207
+ return (field) =>
208
+ ({
209
+ ...defaultCompoundField(field, displayName, false),
210
+ ...other,
211
+ children: fields,
212
+ }) as any;
213
+ }
214
+
215
+ export function defaultScalarField(
216
+ field: string,
217
+ displayName: string,
218
+ ): Omit<SchemaField, "type"> & {
219
+ type: FieldType.String;
220
+ } {
221
+ return {
222
+ field,
223
+ displayName,
224
+ type: FieldType.String,
225
+ };
226
+ }
227
+
228
+ export function defaultCompoundField(
229
+ field: string,
230
+ displayName: string,
231
+ collection: boolean,
232
+ ): CompoundField & {
233
+ type: FieldType.Compound;
234
+ } {
235
+ return {
236
+ field,
237
+ displayName,
238
+ type: FieldType.Compound,
239
+ collection,
240
+ children: [],
241
+ };
242
+ }
243
+
244
+ export function mergeField(
245
+ field: SchemaField,
246
+ mergeInto: SchemaField[],
247
+ ): SchemaField[] {
248
+ const existing = mergeInto.find((x) => x.field === field.field);
249
+ if (existing) {
250
+ return mergeInto.map((x) =>
251
+ x !== existing
252
+ ? x
253
+ : {
254
+ ...x,
255
+ onlyForTypes: mergeTypes(x.onlyForTypes, field.onlyForTypes),
256
+ },
257
+ );
258
+ }
259
+ return [...mergeInto, field];
260
+
261
+ function mergeTypes(f?: string[] | null, s?: string[] | null) {
262
+ if (!f) return s;
263
+ if (!s) return f;
264
+ const extras = s.filter((x) => !f.includes(x));
265
+ return extras.length ? [...f, ...extras] : f;
266
+ }
267
+ }
268
+
269
+ export function mergeFields(
270
+ fields: SchemaField[],
271
+ name: string,
272
+ value: any,
273
+ newFields: SchemaField[],
274
+ ): SchemaField[] {
275
+ const withType = fields.map((x) =>
276
+ x.isTypeField ? addFieldOption(x, name, value) : x,
277
+ );
278
+ return newFields
279
+ .map((x) => ({ ...x, onlyForTypes: [value] }))
280
+ .reduce((af, x) => mergeField(x, af), withType);
281
+ }
282
+
283
+ export function addFieldOption(
284
+ typeField: SchemaField,
285
+ name: string,
286
+ value: any,
287
+ ): SchemaField {
288
+ const options = typeField.options ?? [];
289
+ if (options.some((x) => x.value === value)) return typeField;
290
+ return {
291
+ ...typeField,
292
+ options: [...options, { name, value }],
293
+ };
294
+ }
295
+
296
+ export function resolveSchemas<A extends SchemaMap>(schemaMap: A): A {
297
+ const out: SchemaMap = {};
298
+ function resolveSchemaType(type: string) {
299
+ if (type in out) {
300
+ return out[type];
301
+ }
302
+ const resolvedFields: SchemaField[] = [];
303
+ out[type] = resolvedFields;
304
+ schemaMap[type].forEach((x) => {
305
+ if (isCompoundField(x) && x.schemaRef) {
306
+ resolvedFields.push({
307
+ ...x,
308
+ children: resolveSchemaType(x.schemaRef),
309
+ } as CompoundField);
310
+ } else {
311
+ resolvedFields.push(x);
312
+ }
313
+ });
314
+ return resolvedFields;
315
+ }
316
+ Object.keys(schemaMap).forEach(resolveSchemaType);
317
+ return out as A;
318
+ }
@@ -0,0 +1,188 @@
1
+ import {
2
+ Control,
3
+ ensureMetaValue,
4
+ newControl,
5
+ updateComputedValue,
6
+ } from "@astroapps/controls";
7
+ import { missingField } from "./schemaField";
8
+ import { createSchemaNode, resolveSchemaNode, SchemaNode } from "./schemaNode";
9
+ import { SchemaInterface } from "./schemaInterface";
10
+ import { ControlDefinition, getDisplayOnlyOptions } from "./controlDefinition";
11
+
12
+ export abstract class SchemaDataTree {
13
+ abstract rootNode: SchemaDataNode;
14
+
15
+ abstract getChild(parent: SchemaDataNode, child: SchemaNode): SchemaDataNode;
16
+
17
+ abstract getChildElement(
18
+ parent: SchemaDataNode,
19
+ elementIndex: number,
20
+ ): SchemaDataNode;
21
+ }
22
+
23
+ export class SchemaDataNode {
24
+ constructor(
25
+ public id: string,
26
+ public schema: SchemaNode,
27
+ public elementIndex: number | undefined,
28
+ public control: Control<any>,
29
+ public tree: SchemaDataTree,
30
+ public parent?: SchemaDataNode,
31
+ ) {}
32
+
33
+ getChild(childNode: SchemaNode): SchemaDataNode {
34
+ return this.tree.getChild(this, childNode);
35
+ }
36
+
37
+ getChildElement(elementIndex: number): SchemaDataNode {
38
+ return this.tree.getChildElement(this, elementIndex);
39
+ }
40
+ }
41
+
42
+ export function getMetaFields<
43
+ T extends Record<string, any> = Record<string, unknown>,
44
+ >(control: Control<any>): Control<T> {
45
+ return ensureMetaValue(
46
+ control,
47
+ "metaFields",
48
+ () => newControl({}) as Control<T>,
49
+ );
50
+ }
51
+ export class SchemaDataTreeImpl extends SchemaDataTree {
52
+ rootNode: SchemaDataNode;
53
+
54
+ constructor(rootSchema: SchemaNode, rootControl: Control<any>) {
55
+ super();
56
+ this.rootNode = new SchemaDataNode(
57
+ "",
58
+ rootSchema,
59
+ undefined,
60
+ rootControl,
61
+ this,
62
+ );
63
+ }
64
+
65
+ getChild(parent: SchemaDataNode, childNode: SchemaNode): SchemaDataNode {
66
+ let objControl = parent.control as Control<Record<string, unknown>>;
67
+ if (childNode.field.meta) {
68
+ objControl = getMetaFields(objControl);
69
+ }
70
+ const child = objControl.fields[childNode.field.field];
71
+ return new SchemaDataNode(
72
+ child.uniqueId.toString(),
73
+ childNode,
74
+ undefined,
75
+ child,
76
+ this,
77
+ parent,
78
+ );
79
+ }
80
+
81
+ getChildElement(
82
+ parent: SchemaDataNode,
83
+ elementIndex: number,
84
+ ): SchemaDataNode {
85
+ const elemControl = parent.control as Control<unknown[]>;
86
+ const elemChild = elemControl.elements[elementIndex];
87
+ return new SchemaDataNode(
88
+ elemChild.uniqueId.toString(),
89
+ parent.schema,
90
+ elementIndex,
91
+ elemChild,
92
+ this,
93
+ parent,
94
+ );
95
+ }
96
+ }
97
+
98
+ /**
99
+ * @deprecated Use createSchemaDataNode instead.
100
+ */
101
+ export const makeSchemaDataNode = createSchemaDataNode;
102
+
103
+ export function createSchemaDataNode(
104
+ schema: SchemaNode,
105
+ control: Control<unknown>,
106
+ ): SchemaDataNode {
107
+ return new SchemaDataTreeImpl(schema, control).rootNode;
108
+ }
109
+
110
+ export function schemaDataForFieldRef(
111
+ fieldRef: string | undefined,
112
+ schema: SchemaDataNode,
113
+ ): SchemaDataNode {
114
+ return schemaDataForFieldPath(fieldRef?.split("/") ?? [], schema);
115
+ }
116
+
117
+ export function schemaDataForFieldPath(
118
+ fieldPath: string[],
119
+ dataNode: SchemaDataNode,
120
+ ): SchemaDataNode {
121
+ let i = 0;
122
+ while (i < fieldPath.length) {
123
+ const nextField = fieldPath[i];
124
+ let nextNode =
125
+ nextField === ".."
126
+ ? dataNode.parent
127
+ : nextField === "."
128
+ ? dataNode
129
+ : lookupField(nextField);
130
+ nextNode ??= createSchemaDataNode(
131
+ createSchemaNode(
132
+ missingField(nextField),
133
+ dataNode.schema.tree,
134
+ dataNode.schema,
135
+ ),
136
+ newControl(undefined),
137
+ );
138
+ dataNode = nextNode;
139
+ i++;
140
+ }
141
+ return dataNode;
142
+
143
+ function lookupField(field: string): SchemaDataNode | undefined {
144
+ const childNode = resolveSchemaNode(dataNode.schema, field);
145
+ if (childNode) {
146
+ return dataNode.getChild(childNode);
147
+ }
148
+ return undefined;
149
+ }
150
+ }
151
+
152
+ export function validDataNode(context: SchemaDataNode): boolean {
153
+ const parent = context.parent;
154
+ if (!parent) return true;
155
+ if (parent.schema.field.collection && parent.elementIndex == null)
156
+ return validDataNode(parent);
157
+ return ensureMetaValue(context.control, "validForSchema", () => {
158
+ const c = newControl(true);
159
+ updateComputedValue(c, () => {
160
+ if (!validDataNode(parent)) return false;
161
+ const types = context.schema.field.onlyForTypes;
162
+ if (types == null || types.length === 0) return true;
163
+ const typeNode = parent.schema
164
+ .getChildNodes()
165
+ .find((x) => x.field.isTypeField);
166
+ if (typeNode == null) {
167
+ console.warn("No type field found for", parent.schema);
168
+ return false;
169
+ }
170
+ const typeField = parent.getChild(typeNode).control as Control<string>;
171
+ return typeField && types.includes(typeField.value);
172
+ });
173
+ return c;
174
+ }).value;
175
+ }
176
+
177
+ export function hideDisplayOnly(
178
+ context: SchemaDataNode,
179
+ schemaInterface: SchemaInterface,
180
+ definition: ControlDefinition,
181
+ ) {
182
+ const displayOptions = getDisplayOnlyOptions(definition);
183
+ return (
184
+ displayOptions &&
185
+ !displayOptions.emptyText &&
186
+ schemaInterface.isEmptyValue(context.schema.field, context.control?.value)
187
+ );
188
+ }
@@ -0,0 +1,155 @@
1
+ import { SchemaValidator } from "./schemaValidator";
2
+
3
+ export type EqualityFunc = (a: any, b: any) => boolean;
4
+
5
+ /**
6
+ * Represents a schema field with various properties.
7
+ */
8
+ export interface SchemaField {
9
+ /** The type of the field. */
10
+ type: string;
11
+ /** The name of the field. */
12
+ field: string;
13
+ /** The display name of the field, optional. */
14
+ displayName?: string | null;
15
+ /** Tags associated with the field, optional. */
16
+ tags?: string[] | null;
17
+ /** Indicates if the field is a system field, optional. */
18
+ system?: boolean | null;
19
+ /** Indicates if the field is a meta field, optional. */
20
+ meta?: boolean | null;
21
+ /** Indicates if the field is a collection, optional. */
22
+ collection?: boolean | null;
23
+ /** Specifies the types for which the field is applicable, optional. */
24
+ onlyForTypes?: string[] | null;
25
+ /** Indicates if the field is required, optional. */
26
+ required?: boolean | null;
27
+ /** Indicates if the field is not nullable, optional. */
28
+ notNullable?: boolean | null;
29
+ /** The default value of the field, optional. */
30
+ defaultValue?: any;
31
+ /** Indicates if the field is a type field, optional. */
32
+ isTypeField?: boolean | null;
33
+ /** Indicates if the field is searchable, optional. */
34
+ searchable?: boolean | null;
35
+ /** Options for the field, optional. */
36
+ options?: FieldOption[] | null;
37
+ /** Validators for the field, optional. */
38
+ validators?: SchemaValidator[] | null;
39
+ }
40
+
41
+ /**
42
+ * Represents a map of schema fields.
43
+ * The key is a string representing the schema name.
44
+ * The value is an array of SchemaField objects.
45
+ */
46
+ export type SchemaMap = Record<string, SchemaField[]>;
47
+
48
+ /**
49
+ * Enum representing the various field types.
50
+ */
51
+ export enum FieldType {
52
+ String = "String",
53
+ Bool = "Bool",
54
+ Int = "Int",
55
+ Date = "Date",
56
+ DateTime = "DateTime",
57
+ Time = "Time",
58
+ Double = "Double",
59
+ EntityRef = "EntityRef",
60
+ Compound = "Compound",
61
+ AutoId = "AutoId",
62
+ Image = "Image",
63
+ Any = "Any",
64
+ }
65
+
66
+ /**
67
+ * Represents a field that references an entity.
68
+ */
69
+ export interface EntityRefField extends SchemaField {
70
+ /** The type of the field, which is EntityRef. */
71
+ type: FieldType.EntityRef;
72
+ /** The type of the referenced entity. */
73
+ entityRefType: string;
74
+ /** The parent field of the entity reference. */
75
+ parentField: string;
76
+ }
77
+
78
+ /**
79
+ * Represents an option for a field.
80
+ */
81
+ export interface FieldOption {
82
+ /** The name of the option. */
83
+ name: string;
84
+ /** The value of the option. */
85
+ value: any;
86
+ /** The description of the option, optional. */
87
+ description?: string | null;
88
+ /** The group of the option, optional. */
89
+ group?: string | null;
90
+ /** Indicates if the option is disabled, optional. */
91
+ disabled?: boolean | null;
92
+ }
93
+
94
+ /**
95
+ * Represents a compound field that contains child fields.
96
+ */
97
+ export interface CompoundField extends SchemaField {
98
+ /** The type of the field, which is Compound. */
99
+ type: FieldType.Compound;
100
+ /** The child fields of the compound field. */
101
+ children: SchemaField[];
102
+ /** Indicates if the children are tree-structured, optional. */
103
+ treeChildren?: boolean;
104
+ /** The schema reference for the compound field, optional. */
105
+ schemaRef?: string;
106
+ }
107
+
108
+ /**
109
+ * Enum representing the various validation message types.
110
+ */
111
+ export enum ValidationMessageType {
112
+ NotEmpty = "NotEmpty",
113
+ MinLength = "MinLength",
114
+ MaxLength = "MaxLength",
115
+ NotAfterDate = "NotAfterDate",
116
+ NotBeforeDate = "NotBeforeDate",
117
+ }
118
+
119
+ export function findField(
120
+ fields: SchemaField[],
121
+ field: string,
122
+ ): SchemaField | undefined {
123
+ return fields.find((x) => x.field === field);
124
+ }
125
+
126
+ export function isScalarField(sf: SchemaField): sf is SchemaField {
127
+ return !isCompoundField(sf);
128
+ }
129
+
130
+ export function isCompoundField(sf: SchemaField): sf is CompoundField {
131
+ return sf.type === FieldType.Compound;
132
+ }
133
+
134
+ export function missingField(field: string): SchemaField {
135
+ return { field: "__missing", type: FieldType.Any, displayName: field };
136
+ }
137
+
138
+ export enum SchemaTags {
139
+ NoControl = "_NoControl",
140
+ HtmlEditor = "_HtmlEditor",
141
+ ControlGroup = "_ControlGroup:",
142
+ ControlRef = "_ControlRef:",
143
+ IdField = "_IdField:",
144
+ }
145
+
146
+ export function getTagParam(
147
+ field: SchemaField,
148
+ tag: string,
149
+ ): string | undefined {
150
+ return field.tags?.find((x) => x.startsWith(tag))?.substring(tag.length);
151
+ }
152
+
153
+ export function makeParamTag(tag: string, value: string): string {
154
+ return `${tag}${value}`;
155
+ }