@cubicweb/client 0.1.0

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,414 @@
1
+ import { Cardinality } from "..";
2
+ import { InconsistentSchemaError, RelationNotFoundError } from "../Errors";
3
+ import {
4
+ RawSchema,
5
+ RelationDefinitionRawSchema,
6
+ RelationDefinitionRawSchemaArray,
7
+ EntityRawSchemaArray,
8
+ EntityRawSchemaByType,
9
+ IsLooseSchema,
10
+ IsETypeInStrictSchema,
11
+ } from "./SchemaJSON";
12
+
13
+ const rawSchemaKey: unique symbol = Symbol("Indexer of the rawSchema");
14
+
15
+ /**
16
+ * Returns the given schema using loose typing.
17
+ * Can be useful when working for generic schemas
18
+ *
19
+ * @param schema The schema to create a loose type for
20
+ * @returns
21
+ */
22
+ export const getLooseSchema = <
23
+ E extends EntityRawSchemaArray = EntityRawSchemaArray, // List of all entities
24
+ R extends RelationDefinitionRawSchemaArray<E> = RelationDefinitionRawSchemaArray<E> // List of all relation defintions
25
+ >(
26
+ schema: Schema<E, R>
27
+ ) => {
28
+ return schema as unknown as Schema<
29
+ EntityRawSchemaArray,
30
+ RelationDefinitionRawSchemaArray<EntityRawSchemaArray>
31
+ >;
32
+ };
33
+
34
+ /**
35
+ * Wrapper around the raw JSON Schema
36
+ */
37
+ export class Schema<
38
+ E extends EntityRawSchemaArray = EntityRawSchemaArray, // List of all entities
39
+ R extends RelationDefinitionRawSchemaArray<E> = RelationDefinitionRawSchemaArray<E> // List of all relation defintions
40
+ > {
41
+ [rawSchemaKey]: RawSchema<E, R>;
42
+
43
+ constructor(rawSchema: RawSchema<E, R>) {
44
+ this[rawSchemaKey] = rawSchema;
45
+ }
46
+
47
+ get entities(): EntitySchema<E, R, E[number]>[] {
48
+ return this[rawSchemaKey].entities.map(
49
+ (eSchema) => new EntitySchema(this, eSchema)
50
+ );
51
+ }
52
+
53
+ get relationsDefinitions(): RelationDefinitionSchema<E, R, R[number]>[] {
54
+ return this[rawSchemaKey].relations_definitions.map(
55
+ (rdefSchema) => new RelationDefinitionSchema(this, rdefSchema)
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Returns the entity schema for the given entity type if it exists.
61
+ * When using TypeScript, this method can detect if the given type does not exist
62
+ * in the schema.
63
+ *
64
+ * @param entityType The entity type string
65
+ * @returns A new EntitySchema or null if the type does not exist
66
+ */
67
+ getEntity<EType extends string>(
68
+ entityType: EType
69
+ ): IsLooseSchema<E> extends true
70
+ ? EntitySchema<E, R, E[number]> | null
71
+ : IsETypeInStrictSchema<E, EType> extends true
72
+ ? EntitySchema<E, R, EntityRawSchemaByType<E, EType>>
73
+ : null {
74
+ const entityRawSchema = this[rawSchemaKey].entities.filter(
75
+ (eSchema) => eSchema.type === entityType
76
+ )[0];
77
+ if (!entityRawSchema) {
78
+ // Is only null when the given type is not in the schema.
79
+ // Thus the return type will be (EntitySchema | null)
80
+ // but TypeScript does not understand this
81
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
82
+ // @ts-ignore
83
+ return null;
84
+ }
85
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
86
+ // @ts-ignore
87
+ return new EntitySchema<E, R, E[number]>(
88
+ this,
89
+ entityRawSchema as EntityRawSchemaByType<E, EType>
90
+ );
91
+ }
92
+
93
+ /**
94
+ * Finds relations in the Schema matching the partial relation given
95
+ *
96
+ * @param matcher Partial relation definition to match against
97
+ * @returns Matched relation schema classes
98
+ */
99
+ matchRelationDefinitions<M extends RelationDefinitionMatcher<E>>(
100
+ matcher: M
101
+ ): RelationDefinitionSchema<
102
+ E,
103
+ R,
104
+ MatchedRelationDefinition<E, R, { subject: E[number]["type"] }>
105
+ >[] {
106
+ const matchArray = this[rawSchemaKey].relations_definitions.filter(
107
+ (relDef) => {
108
+ for (const key of Object.keys(
109
+ matcher
110
+ ) as (keyof RelationDefinitionRawSchema<E>)[]) {
111
+ if (matcher[key] !== relDef[key]) {
112
+ return false;
113
+ }
114
+ }
115
+ return true;
116
+ }
117
+ ) as MatchedRelationDefinition<
118
+ E,
119
+ R,
120
+ MatchedRelationDefinition<E, R, { subject: E[number]["type"] }>
121
+ >[];
122
+ return matchArray.map((m) => new RelationDefinitionSchema(this, m));
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Wrapper around raw entities from the Schema
128
+ */
129
+ export class EntitySchema<
130
+ E extends EntityRawSchemaArray = EntityRawSchemaArray, // List of all entities
131
+ R extends RelationDefinitionRawSchemaArray<E> = RelationDefinitionRawSchemaArray<E>, // List of all relation defintions
132
+ ERawSchema extends E[number] = E[number] // JSON of the represented entity
133
+ > {
134
+ constructor(
135
+ private schema: Schema<E, R>,
136
+ private entityRawSchema: ERawSchema
137
+ ) {}
138
+
139
+ /**
140
+ * The type of the entity. This can also be referred to as the entity' name.
141
+ */
142
+ get type(): ERawSchema["type"] {
143
+ return this.entityRawSchema.type;
144
+ }
145
+
146
+ /**
147
+ * A short description which can be used as documentation for the entity.
148
+ */
149
+ get description(): ERawSchema["description"] {
150
+ return this.entityRawSchema.description;
151
+ }
152
+
153
+ /**
154
+ * True if the current entity is a primitive type such as string or int
155
+ */
156
+ get final(): ERawSchema["final"] {
157
+ return this.entityRawSchema.final;
158
+ }
159
+
160
+ /**
161
+ * Gets the list of relation definitions with this entity as subject
162
+ */
163
+ get subjectRelationDefinitions(): RelationDefinitionSchema<
164
+ E,
165
+ R,
166
+ MatchedRelationDefinition<E, R, { subject: ERawSchema["type"] }>
167
+ >[] {
168
+ return this.schema.matchRelationDefinitions({
169
+ subject: this.type,
170
+ final: false,
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Gets the list of relation definitions with this entity as object
176
+ */
177
+ get objectRelationsDefinitions(): RelationDefinitionSchema<
178
+ E,
179
+ R,
180
+ MatchedRelationDefinition<E, R, { object: ERawSchema["type"] }>
181
+ >[] {
182
+ return this.schema.matchRelationDefinitions({
183
+ object: this.type,
184
+ final: false,
185
+ });
186
+ }
187
+
188
+ /**
189
+ * Finds the first relation of the given type with this entity as subject
190
+ *
191
+ * @param relationType The relation type to find
192
+ * @throws RelationNotFoundError if the current entity is not a subject of the given relation
193
+ * @returns The found relation
194
+ */
195
+ getSubjectRelationDefinition<
196
+ RelationType extends MatchedRelationDefinition<
197
+ E,
198
+ R,
199
+ { subject: ERawSchema["type"] }
200
+ >["type"]
201
+ >(
202
+ relationType: RelationType
203
+ ): RelationDefinitionSchema<
204
+ E,
205
+ R,
206
+ MatchedRelationDefinition<
207
+ E,
208
+ R,
209
+ { subject: ERawSchema["type"]; type: RelationType }
210
+ >
211
+ > {
212
+ const resultArray = this.schema.matchRelationDefinitions({
213
+ subject: this.type,
214
+ type: relationType,
215
+ });
216
+ if (resultArray.length > 0) {
217
+ return resultArray[0];
218
+ }
219
+ throw new RelationNotFoundError(relationType, "subject", this.type);
220
+ }
221
+
222
+ /**
223
+ * Finds the first relation of the given type with this entity as object
224
+ *
225
+ * @param relationType The relation type to find
226
+ * @throws RelationNotFoundError if the current entity is not an object of the given relation
227
+ * @returns The found relation
228
+ */
229
+ getObjectRelationDefinition<
230
+ RelationType extends MatchedRelationDefinition<
231
+ E,
232
+ R,
233
+ { object: ERawSchema["type"] }
234
+ >["type"]
235
+ >(
236
+ relationType: RelationType
237
+ ): RelationDefinitionSchema<
238
+ E,
239
+ R,
240
+ MatchedRelationDefinition<
241
+ E,
242
+ R,
243
+ { object: ERawSchema["type"]; type: RelationType }
244
+ >
245
+ > {
246
+ const resultArray = this.schema.matchRelationDefinitions({
247
+ object: this.type,
248
+ type: relationType,
249
+ });
250
+ if (resultArray.length > 0) {
251
+ return resultArray[0];
252
+ }
253
+ throw new RelationNotFoundError(relationType, "object", this.type);
254
+ }
255
+
256
+ /**
257
+ * Gets the list of attributes names for this entity
258
+ */
259
+ get attributes(): RelationDefinitionSchema<E, R, R[number]>[] {
260
+ return this.schema.matchRelationDefinitions({
261
+ final: true,
262
+ subject: this.type,
263
+ });
264
+ }
265
+
266
+ /**
267
+ * Checks whether the current entity is required for the given relation and role
268
+ *
269
+ * @param relationType The relation to check
270
+ * @param role The role for this entity in the given relation
271
+ * @returns True if this entity is required in the relation for the given role
272
+ */
273
+ isRelationRequired<
274
+ RelationType extends MatchedRelationDefinition<
275
+ E,
276
+ R,
277
+ { subject: ERawSchema["type"] } | { object: ERawSchema["type"] }
278
+ >["type"]
279
+ >(relationType: RelationType, role: "subject" | "object"): boolean {
280
+ let relation;
281
+ let cardinality: Cardinality;
282
+ if (role === "subject") {
283
+ relation = this.getSubjectRelationDefinition(relationType);
284
+ cardinality = relation.cardinality[0];
285
+ } else {
286
+ relation = this.getObjectRelationDefinition(relationType);
287
+ cardinality = relation.cardinality[1];
288
+ }
289
+ return cardinalityRequired(cardinality);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Wrapper around raw relation definitions from the Schema
295
+ */
296
+ export class RelationDefinitionSchema<
297
+ E extends EntityRawSchemaArray = EntityRawSchemaArray, // List of all entities
298
+ R extends RelationDefinitionRawSchemaArray<E> = RelationDefinitionRawSchemaArray<E>, // List of all relation defintions
299
+ RDef extends R[number] = R[number] // JSON of the represented relation definition
300
+ > {
301
+ constructor(private schema: Schema<E, R>, private relDef: RDef) {}
302
+
303
+ /**
304
+ * The type of the relation. This can also be referred to as the relation's name.
305
+ */
306
+ get type(): RDef["type"] {
307
+ return this.relDef.type;
308
+ }
309
+
310
+ /**
311
+ * A short description which can be used as documentation for the relation.
312
+ */
313
+ get description(): RDef["description"] {
314
+ return this.relDef.description;
315
+ }
316
+
317
+ /**
318
+ * True if the object of the relation is a primitive type.
319
+ * In this case the relation iscalled an "attribute"
320
+ */
321
+ get final(): RDef["final"] {
322
+ return this.relDef.final;
323
+ }
324
+
325
+ /**
326
+ * The list of constraints if any. Only attributes (relations with final = true) have constraints.
327
+ * In other cases this returns null.
328
+ */
329
+ get constraints(): RDef["constraints"] {
330
+ return this.relDef.constraints;
331
+ }
332
+
333
+ /**
334
+ * The default value for this relation.
335
+ * It can be used when creating new relations if user did not enter a value (even if required).
336
+ */
337
+ get default(): RDef extends { default: infer V } ? V : undefined {
338
+ type Default = RDef extends { default: infer V } ? V : undefined;
339
+
340
+ return (
341
+ "default" in this.relDef ? this.relDef.default : undefined
342
+ ) as Default;
343
+ }
344
+
345
+ get cardinality(): RDef["cardinality"] {
346
+ return this.relDef.cardinality;
347
+ }
348
+
349
+ get subjectCardinality(): Cardinality {
350
+ return this.relDef.cardinality[0] as Cardinality;
351
+ }
352
+
353
+ get objectCardinality(): Cardinality {
354
+ return this.relDef.cardinality[1] as Cardinality;
355
+ }
356
+
357
+ /**
358
+ * Gets the entity associated to this relation's subject
359
+ *
360
+ * @throws InconsistentSchemaError if the relation's subject is not a valid entity
361
+ */
362
+ get subject(): IsLooseSchema<E> extends true
363
+ ? EntitySchema<E, R, E[number]>
364
+ : EntitySchema<E, R, EntityRawSchemaByType<E, RDef["subject"]>> {
365
+ const subjectSchema = this.schema.getEntity(this.relDef.subject);
366
+ if (subjectSchema === null) {
367
+ throw new InconsistentSchemaError(
368
+ `entity type ${this.relDef.subject} used as subject of relation ${this.type} is not declared`
369
+ );
370
+ }
371
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
372
+ // @ts-ignore
373
+ return subjectSchema;
374
+ }
375
+
376
+ /**
377
+ * Gets the entity associated to this relation's object
378
+ *
379
+ * @throws InconsistentSchemaError if the relation's object is not a valid entity
380
+ */
381
+ get object(): IsLooseSchema<E> extends true
382
+ ? EntitySchema<E, R, E[number]>
383
+ : EntitySchema<E, R, EntityRawSchemaByType<E, RDef["object"]>> {
384
+ const objectSchema = this.schema.getEntity(this.relDef.object);
385
+ if (objectSchema === null) {
386
+ throw new InconsistentSchemaError(
387
+ `entity type ${this.relDef.object} used as object of relation ${this.type} is not declared`
388
+ );
389
+ }
390
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
391
+ // @ts-ignore
392
+ return objectSchema;
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Checks if the given cardinality requires at least one entity to be present
398
+ *
399
+ * @param cardinality The cardinality to check
400
+ * @returns
401
+ */
402
+ function cardinalityRequired(cardinality: Cardinality): boolean {
403
+ return cardinality === "1" || cardinality === "+";
404
+ }
405
+
406
+ export type RelationDefinitionMatcher<E extends EntityRawSchemaArray> = Partial<
407
+ RelationDefinitionRawSchema<E>
408
+ >;
409
+
410
+ export type MatchedRelationDefinition<
411
+ E extends EntityRawSchemaArray,
412
+ R extends RelationDefinitionRawSchemaArray<EntityRawSchemaArray>,
413
+ M extends RelationDefinitionMatcher<E>
414
+ > = R[number] extends infer RDef ? (RDef extends M ? RDef : never) : never;
@@ -0,0 +1,195 @@
1
+ type ConstraintBase<
2
+ T extends string,
3
+ V extends Record<string, unknown> | null
4
+ > = {
5
+ type: T;
6
+ value: V;
7
+ };
8
+
9
+ export type ConstraintOperator = ">" | ">=" | "<" | "<=";
10
+ type ConstraintMessage = string | null;
11
+
12
+ export type UniqueConstraint = ConstraintBase<"UniqueConstraint", null>;
13
+
14
+ export type SizeConstraint = ConstraintBase<
15
+ "SizeConstraint",
16
+ { min: number | null; max: number | null; msg: ConstraintMessage }
17
+ >;
18
+
19
+ export type RegexpConstraint = ConstraintBase<
20
+ "RegexpConstraint",
21
+ { regexp: string; flags: number; msg: ConstraintMessage }
22
+ >;
23
+
24
+ export type BoundaryConstraint = ConstraintBase<
25
+ "BoundaryConstraint",
26
+ { boundary: number | null; op: ConstraintOperator; msg: ConstraintMessage }
27
+ >;
28
+ export type IntervalBoundConstraint = ConstraintBase<
29
+ "IntervalBoundConstraint",
30
+ { minvalue: number | null; maxvalue: number | null; msg: ConstraintMessage }
31
+ >;
32
+
33
+ export type StaticVocabularyConstraint = ConstraintBase<
34
+ "StaticVocabularyConstraint",
35
+ { values: string[]; msg: ConstraintMessage }
36
+ >;
37
+
38
+ export type FormatConstraint = ConstraintBase<
39
+ "FormatConstraint",
40
+ {
41
+ values: ["text/rest", "text/markdown", "text/html", "text/plain"];
42
+ msg: ConstraintMessage;
43
+ }
44
+ >;
45
+
46
+ export type Constraint = Readonly<
47
+ | UniqueConstraint
48
+ | SizeConstraint
49
+ | RegexpConstraint
50
+ | BoundaryConstraint
51
+ | IntervalBoundConstraint
52
+ | StaticVocabularyConstraint
53
+ | FormatConstraint
54
+ >;
55
+
56
+ type BuildObjBase<T extends string> = {
57
+ object: T;
58
+ final: true;
59
+ required?: boolean;
60
+ };
61
+
62
+ interface StringBuildObj extends BuildObjBase<"String"> {
63
+ default: string | null;
64
+ }
65
+
66
+ interface PasswordBuildObj extends BuildObjBase<"Password"> {
67
+ default: string | null;
68
+ }
69
+
70
+ interface BytesBuildObj extends BuildObjBase<"Bytes"> {
71
+ default: string | null;
72
+ }
73
+
74
+ interface IntBuildObj extends BuildObjBase<"Int"> {
75
+ default: number | null;
76
+ }
77
+
78
+ interface BigIntBuildObj extends BuildObjBase<"BigInt"> {
79
+ default: number | null;
80
+ }
81
+
82
+ interface FloatBuildObj extends BuildObjBase<"Float"> {
83
+ default: number | null;
84
+ }
85
+
86
+ interface BooleanBuildObj extends BuildObjBase<"Boolean"> {
87
+ default: boolean | null;
88
+ }
89
+
90
+ interface DecimalBuildObj extends BuildObjBase<"Decimal"> {
91
+ default: number | null;
92
+ }
93
+
94
+ interface DateBuildObj extends BuildObjBase<"Date"> {
95
+ default: unknown;
96
+ }
97
+
98
+ interface TimeBuildObj extends BuildObjBase<"Time"> {
99
+ default: unknown;
100
+ }
101
+
102
+ interface DatetimeBuildObj extends BuildObjBase<"Datetime"> {
103
+ default: Date | "NOW" | "TODAY" | null;
104
+ }
105
+
106
+ interface TZTimeBuildObj extends BuildObjBase<"TZTime"> {
107
+ default: unknown;
108
+ }
109
+
110
+ interface TZDatetimeBuildObj extends BuildObjBase<"TZDatetime"> {
111
+ default: unknown;
112
+ }
113
+
114
+ interface IntervalBuildObj extends BuildObjBase<"Interval"> {
115
+ default: unknown;
116
+ }
117
+
118
+ export type BuildObj = Readonly<
119
+ | StringBuildObj
120
+ | PasswordBuildObj
121
+ | BytesBuildObj
122
+ | IntBuildObj
123
+ | BigIntBuildObj
124
+ | FloatBuildObj
125
+ | BooleanBuildObj
126
+ | DecimalBuildObj
127
+ | DateBuildObj
128
+ | TimeBuildObj
129
+ | DatetimeBuildObj
130
+ | TZTimeBuildObj
131
+ | TZDatetimeBuildObj
132
+ | IntervalBuildObj
133
+ >;
134
+
135
+ export type Cardinality = "*" | "1" | "?" | "+";
136
+
137
+ export type CardinalityPair = `${Cardinality}${Cardinality}`;
138
+
139
+ export type EntityRawSchema = {
140
+ type: string;
141
+ final: boolean;
142
+ description: string;
143
+ };
144
+
145
+ export type EntityRawSchemaArray = ReadonlyArray<EntityRawSchema>;
146
+
147
+ export type RelationDefinitionRawSchema<
148
+ ESchemas extends EntityRawSchemaArray,
149
+ Final extends boolean = boolean
150
+ > = Readonly<
151
+ {
152
+ type: string;
153
+ description: string;
154
+ cardinality: CardinalityPair;
155
+ subject: ESchemas[number]["type"];
156
+ } & (Final extends true
157
+ ? BuildObj & {
158
+ constraints: Constraint[];
159
+ }
160
+ : { object: EntityRawSchema["type"]; final: false; constraints: null })
161
+ >;
162
+
163
+ export type RelationDefinitionRawSchemaArray<
164
+ ESchemas extends EntityRawSchemaArray
165
+ > = ReadonlyArray<RelationDefinitionRawSchema<ESchemas, boolean>>;
166
+
167
+ export type RawSchema<
168
+ E extends EntityRawSchemaArray = EntityRawSchemaArray,
169
+ R extends RelationDefinitionRawSchemaArray<E> = RelationDefinitionRawSchemaArray<E>
170
+ > = {
171
+ entities: E;
172
+ relations_definitions: R;
173
+ };
174
+
175
+ export type IsLooseSchema<E extends EntityRawSchemaArray> =
176
+ string extends E[number]["type"] ? true : false;
177
+
178
+ export type IsETypeInStrictSchema<
179
+ E extends EntityRawSchemaArray,
180
+ EType extends string
181
+ > = EType extends E[number]["type"] ? true : false;
182
+
183
+ /**
184
+ * Type representing the entity's raw schema matching this type.
185
+ * This type only works for EntityRawSchemaArray with literals as "type" property.
186
+ *
187
+ **/
188
+ export type EntityRawSchemaByType<
189
+ E extends EntityRawSchemaArray,
190
+ T extends string
191
+ > = E[number] extends infer ESchema
192
+ ? ESchema extends { type: T }
193
+ ? ESchema
194
+ : never
195
+ : never; // Will never happen E[number] always has a type