@clivly/core 0.1.0 → 0.2.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,272 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/entity-config.d.ts
4
+ /**
5
+ * Entity Config v2 — the declarative contract that turns Clivly's "schema-aware"
6
+ * promise into a real capability.
7
+ *
8
+ * A CRM entity is no longer a flat table name. It is a *derived entity*: a
9
+ * filtered slice of a host table (`source` + `filter`), a column allowlist
10
+ * (`fields` + `readOnly`), and declared `relationships` to other entities. The
11
+ * same host table can back several entities (e.g. `users` → owners + members)
12
+ * distinguished by `role`.
13
+ *
14
+ * This module is the Phase 1 slice: types, schema, `defineClivlyConfig` (a
15
+ * structural helper) and `validateEntitiesConfig` (semantic validation against
16
+ * the host's discovered schema). It performs NO DB access and generates no SQL —
17
+ * the view compiler (Phase 2) and sync engine (Phase 3) consume this contract.
18
+ */
19
+ /** Canonical CRM concepts a host entity can map onto. */
20
+ declare const CRM_CONCEPTS: readonly ["contact", "company", "deal", "activity", "conversation", "note"];
21
+ type CrmConcept = (typeof CRM_CONCEPTS)[number];
22
+ /**
23
+ * The canonical `fields` keys the mirror actually persists, per concept — the
24
+ * crm_* columns the sync store writes. An entity's `fields` map keys must be one
25
+ * of these: a non-canonical key (e.g. `fullName` instead of `name`) validates
26
+ * structurally but has no destination column, so the store silently writes an
27
+ * empty value. `validateEntitiesConfig` flags those.
28
+ *
29
+ * This is the single source of truth — the Drizzle store imports it, so the
30
+ * two can't drift (the drift is what made the silent-data-loss trap possible).
31
+ * Concepts absent here aren't materialized yet, so their field keys are
32
+ * unconstrained.
33
+ */
34
+ declare const CANONICAL_FIELDS: {
35
+ readonly contact: readonly ["name", "email", "title", "status"];
36
+ readonly company: readonly ["name", "domain", "industry", "size"];
37
+ };
38
+ declare const filterValueSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodUnion<readonly [z.ZodObject<{
39
+ eq: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
40
+ }, z.core.$strict>, z.ZodObject<{
41
+ ne: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
42
+ }, z.core.$strict>, z.ZodObject<{
43
+ in: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
44
+ }, z.core.$strict>, z.ZodObject<{
45
+ notIn: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
46
+ }, z.core.$strict>, z.ZodObject<{
47
+ isNull: z.ZodLiteral<true>;
48
+ }, z.core.$strict>, z.ZodObject<{
49
+ isNotNull: z.ZodLiteral<true>;
50
+ }, z.core.$strict>]>]>;
51
+ /** `$raw` is a reserved key; a column literally named `$raw` is not addressable. */
52
+ declare const filterSchema: z.ZodUnion<readonly [z.ZodObject<{
53
+ $raw: z.ZodString;
54
+ }, z.core.$strict>, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodUnion<readonly [z.ZodObject<{
55
+ eq: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
56
+ }, z.core.$strict>, z.ZodObject<{
57
+ ne: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
58
+ }, z.core.$strict>, z.ZodObject<{
59
+ in: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
60
+ }, z.core.$strict>, z.ZodObject<{
61
+ notIn: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
62
+ }, z.core.$strict>, z.ZodObject<{
63
+ isNull: z.ZodLiteral<true>;
64
+ }, z.core.$strict>, z.ZodObject<{
65
+ isNotNull: z.ZodLiteral<true>;
66
+ }, z.core.$strict>]>]>>]>;
67
+ type FilterExpr = z.infer<typeof filterSchema>;
68
+ /** A single column predicate value (scalar shorthand or operator object). */
69
+ type FilterValue = z.infer<typeof filterValueSchema>;
70
+ /**
71
+ * How the link between two entities is expressed in the host schema:
72
+ * - `fkOn: "company"` — FK lives on the company table (e.g. organizations.owner_id)
73
+ * - `fkOn: "contact"` — FK lives on the contact table (e.g. users.organization_id)
74
+ * - `through` — a join table sits between them (many-to-many, role often here)
75
+ * - `$raw` — opaque SQL join fragment (escape hatch; not schema-validated)
76
+ *
77
+ * `column` and the join-table keys are `table.column` or bare `column` strings.
78
+ */
79
+ declare const relationshipViaSchema: z.ZodUnion<readonly [z.ZodObject<{
80
+ fkOn: z.ZodEnum<{
81
+ contact: "contact";
82
+ company: "company";
83
+ }>;
84
+ column: z.ZodString;
85
+ }, z.core.$strict>, z.ZodObject<{
86
+ through: z.ZodString;
87
+ localKey: z.ZodString;
88
+ foreignKey: z.ZodString;
89
+ }, z.core.$strict>, z.ZodObject<{
90
+ $raw: z.ZodString;
91
+ }, z.core.$strict>]>;
92
+ type RelationshipVia = z.infer<typeof relationshipViaSchema>;
93
+ declare const relationshipSchema: z.ZodObject<{
94
+ entity: z.ZodString;
95
+ via: z.ZodUnion<readonly [z.ZodObject<{
96
+ fkOn: z.ZodEnum<{
97
+ contact: "contact";
98
+ company: "company";
99
+ }>;
100
+ column: z.ZodString;
101
+ }, z.core.$strict>, z.ZodObject<{
102
+ through: z.ZodString;
103
+ localKey: z.ZodString;
104
+ foreignKey: z.ZodString;
105
+ }, z.core.$strict>, z.ZodObject<{
106
+ $raw: z.ZodString;
107
+ }, z.core.$strict>]>;
108
+ }, z.core.$strict>;
109
+ type RelationshipSpec = z.infer<typeof relationshipSchema>;
110
+ /** A named map of relationships, as stored on an entity / in the mappings row. */
111
+ declare const relationshipsSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
112
+ entity: z.ZodString;
113
+ via: z.ZodUnion<readonly [z.ZodObject<{
114
+ fkOn: z.ZodEnum<{
115
+ contact: "contact";
116
+ company: "company";
117
+ }>;
118
+ column: z.ZodString;
119
+ }, z.core.$strict>, z.ZodObject<{
120
+ through: z.ZodString;
121
+ localKey: z.ZodString;
122
+ foreignKey: z.ZodString;
123
+ }, z.core.$strict>, z.ZodObject<{
124
+ $raw: z.ZodString;
125
+ }, z.core.$strict>]>;
126
+ }, z.core.$strict>>;
127
+ type RelationshipsMap = z.infer<typeof relationshipsSchema>;
128
+ declare const entitySchema: z.ZodObject<{
129
+ concept: z.ZodEnum<{
130
+ contact: "contact";
131
+ company: "company";
132
+ deal: "deal";
133
+ activity: "activity";
134
+ conversation: "conversation";
135
+ note: "note";
136
+ }>;
137
+ source: z.ZodString;
138
+ role: z.ZodOptional<z.ZodString>;
139
+ filter: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
140
+ $raw: z.ZodString;
141
+ }, z.core.$strict>, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodUnion<readonly [z.ZodObject<{
142
+ eq: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
143
+ }, z.core.$strict>, z.ZodObject<{
144
+ ne: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
145
+ }, z.core.$strict>, z.ZodObject<{
146
+ in: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
147
+ }, z.core.$strict>, z.ZodObject<{
148
+ notIn: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
149
+ }, z.core.$strict>, z.ZodObject<{
150
+ isNull: z.ZodLiteral<true>;
151
+ }, z.core.$strict>, z.ZodObject<{
152
+ isNotNull: z.ZodLiteral<true>;
153
+ }, z.core.$strict>]>]>>]>>;
154
+ fields: z.ZodRecord<z.ZodString, z.ZodString>;
155
+ readOnly: z.ZodOptional<z.ZodArray<z.ZodString>>;
156
+ relationships: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
157
+ entity: z.ZodString;
158
+ via: z.ZodUnion<readonly [z.ZodObject<{
159
+ fkOn: z.ZodEnum<{
160
+ contact: "contact";
161
+ company: "company";
162
+ }>;
163
+ column: z.ZodString;
164
+ }, z.core.$strict>, z.ZodObject<{
165
+ through: z.ZodString;
166
+ localKey: z.ZodString;
167
+ foreignKey: z.ZodString;
168
+ }, z.core.$strict>, z.ZodObject<{
169
+ $raw: z.ZodString;
170
+ }, z.core.$strict>]>;
171
+ }, z.core.$strict>>>;
172
+ }, z.core.$strict>;
173
+ type ClivlyEntityConfig = z.infer<typeof entitySchema>;
174
+ declare const entitiesConfigSchema: z.ZodObject<{
175
+ entities: z.ZodRecord<z.ZodString, z.ZodObject<{
176
+ concept: z.ZodEnum<{
177
+ contact: "contact";
178
+ company: "company";
179
+ deal: "deal";
180
+ activity: "activity";
181
+ conversation: "conversation";
182
+ note: "note";
183
+ }>;
184
+ source: z.ZodString;
185
+ role: z.ZodOptional<z.ZodString>;
186
+ filter: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
187
+ $raw: z.ZodString;
188
+ }, z.core.$strict>, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodUnion<readonly [z.ZodObject<{
189
+ eq: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
190
+ }, z.core.$strict>, z.ZodObject<{
191
+ ne: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
192
+ }, z.core.$strict>, z.ZodObject<{
193
+ in: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
194
+ }, z.core.$strict>, z.ZodObject<{
195
+ notIn: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
196
+ }, z.core.$strict>, z.ZodObject<{
197
+ isNull: z.ZodLiteral<true>;
198
+ }, z.core.$strict>, z.ZodObject<{
199
+ isNotNull: z.ZodLiteral<true>;
200
+ }, z.core.$strict>]>]>>]>>;
201
+ fields: z.ZodRecord<z.ZodString, z.ZodString>;
202
+ readOnly: z.ZodOptional<z.ZodArray<z.ZodString>>;
203
+ relationships: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
204
+ entity: z.ZodString;
205
+ via: z.ZodUnion<readonly [z.ZodObject<{
206
+ fkOn: z.ZodEnum<{
207
+ contact: "contact";
208
+ company: "company";
209
+ }>;
210
+ column: z.ZodString;
211
+ }, z.core.$strict>, z.ZodObject<{
212
+ through: z.ZodString;
213
+ localKey: z.ZodString;
214
+ foreignKey: z.ZodString;
215
+ }, z.core.$strict>, z.ZodObject<{
216
+ $raw: z.ZodString;
217
+ }, z.core.$strict>]>;
218
+ }, z.core.$strict>>>;
219
+ }, z.core.$strict>>;
220
+ }, z.core.$strict>;
221
+ type ClivlyEntitiesConfig = z.infer<typeof entitiesConfigSchema>;
222
+ /** Thrown by `defineClivlyConfig({ strict: true })` on non-canonical field keys. */
223
+ declare class ClivlyConfigError extends Error {
224
+ readonly issues: ConfigValidationError[];
225
+ constructor(issues: ConfigValidationError[]);
226
+ }
227
+ /**
228
+ * Structural helper for `clivly.config.ts`. Gives full type inference on the
229
+ * literal you pass and eagerly parses it, so shape errors surface at config
230
+ * load rather than at sync time.
231
+ *
232
+ * It also catches the silent-data-loss footgun: a non-canonical field key for a
233
+ * materialised concept (e.g. `fullName` instead of `name` on a `contact`) has no
234
+ * destination column, so the store would write an empty value. By default these
235
+ * are reported via `onWarn` (a `console.warn`); pass `{ strict: true }` to throw
236
+ * a `ClivlyConfigError` instead. Does NOT validate against the host schema — that
237
+ * needs the discovered schema; see `validateEntitiesConfig`.
238
+ */
239
+ declare function defineClivlyConfig(config: ClivlyEntitiesConfig, options?: {
240
+ strict?: boolean;
241
+ onWarn?: (message: string) => void;
242
+ }): ClivlyEntitiesConfig;
243
+ /**
244
+ * A host table the SDK reported for discovery. Structurally identical to the
245
+ * SDK's `DiscoveredTable`; duplicated here to keep `@clivly/core` free of an
246
+ * SDK dependency (core is the base contract SDK builds on).
247
+ */
248
+ interface DiscoveredSchemaTable {
249
+ columns: string[];
250
+ name: string;
251
+ }
252
+ interface ConfigValidationError {
253
+ message: string;
254
+ /** Dotted path into the config, e.g. `entities.owner.fields.email`. */
255
+ path: string;
256
+ }
257
+ interface ConfigValidationResult {
258
+ errors: ConfigValidationError[];
259
+ valid: boolean;
260
+ }
261
+ /**
262
+ * Validate a (structurally valid) config against the host's discovered schema:
263
+ * source tables exist, mapped columns exist, `readOnly` ⊆ `fields`, filter
264
+ * columns exist, and relationships point at real entities/tables/columns.
265
+ *
266
+ * `$raw` filters and `$raw` relationship joins are opaque and skipped. Returns
267
+ * every error found (not just the first) so a mapping UI or CLI can show them
268
+ * all at once.
269
+ */
270
+ declare function validateEntitiesConfig(config: ClivlyEntitiesConfig, schema: DiscoveredSchemaTable[]): ConfigValidationResult;
271
+ //#endregion
272
+ export { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
@@ -219,13 +219,27 @@ declare const entitiesConfigSchema: z.ZodObject<{
219
219
  }, z.core.$strict>>;
220
220
  }, z.core.$strict>;
221
221
  type ClivlyEntitiesConfig = z.infer<typeof entitiesConfigSchema>;
222
+ /** Thrown by `defineClivlyConfig({ strict: true })` on non-canonical field keys. */
223
+ declare class ClivlyConfigError extends Error {
224
+ readonly issues: ConfigValidationError[];
225
+ constructor(issues: ConfigValidationError[]);
226
+ }
222
227
  /**
223
228
  * Structural helper for `clivly.config.ts`. Gives full type inference on the
224
229
  * literal you pass and eagerly parses it, so shape errors surface at config
225
- * load rather than at sync time. Does NOT validate against the host schema —
226
- * that needs the discovered schema; see `validateEntitiesConfig`.
230
+ * load rather than at sync time.
231
+ *
232
+ * It also catches the silent-data-loss footgun: a non-canonical field key for a
233
+ * materialised concept (e.g. `fullName` instead of `name` on a `contact`) has no
234
+ * destination column, so the store would write an empty value. By default these
235
+ * are reported via `onWarn` (a `console.warn`); pass `{ strict: true }` to throw
236
+ * a `ClivlyConfigError` instead. Does NOT validate against the host schema — that
237
+ * needs the discovered schema; see `validateEntitiesConfig`.
227
238
  */
228
- declare function defineClivlyConfig(config: ClivlyEntitiesConfig): ClivlyEntitiesConfig;
239
+ declare function defineClivlyConfig(config: ClivlyEntitiesConfig, options?: {
240
+ strict?: boolean;
241
+ onWarn?: (message: string) => void;
242
+ }): ClivlyEntitiesConfig;
229
243
  /**
230
244
  * A host table the SDK reported for discovery. Structurally identical to the
231
245
  * SDK's `DiscoveredTable`; duplicated here to keep `@clivly/core` free of an
@@ -255,4 +269,4 @@ interface ConfigValidationResult {
255
269
  */
256
270
  declare function validateEntitiesConfig(config: ClivlyEntitiesConfig, schema: DiscoveredSchemaTable[]): ConfigValidationResult;
257
271
  //#endregion
258
- export { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
272
+ export { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
@@ -146,14 +146,56 @@ const entitySchema = z.strictObject({
146
146
  relationships: z.record(z.string(), relationshipSchema).optional()
147
147
  });
148
148
  const entitiesConfigSchema = z.strictObject({ entities: z.record(z.string(), entitySchema).refine((e) => Object.keys(e).length > 0, { message: "at least one entity must be defined" }) });
149
+ /** Thrown by `defineClivlyConfig({ strict: true })` on non-canonical field keys. */
150
+ var ClivlyConfigError = class extends Error {
151
+ issues;
152
+ constructor(issues) {
153
+ super(`Clivly config has ${issues.length} non-canonical field mapping(s) that would write empty values at sync time:\n${issues.map((issue) => ` - ${issue.path}: ${issue.message}`).join("\n")}`);
154
+ this.name = "ClivlyConfigError";
155
+ this.issues = issues;
156
+ }
157
+ };
158
+ function nonCanonicalFieldKeys(entity) {
159
+ const canonical = CANONICAL_FIELDS[entity.concept];
160
+ if (!canonical) return [];
161
+ return Object.keys(entity.fields).filter((key) => !canonical.includes(key));
162
+ }
163
+ function nonCanonicalMessage(entity, field) {
164
+ const canonical = CANONICAL_FIELDS[entity.concept] ?? [];
165
+ return `"${field}" is not a canonical ${entity.concept} field (expected one of: ${canonical.join(", ")})`;
166
+ }
167
+ function canonicalFieldIssues(config) {
168
+ const issues = [];
169
+ for (const [key, entity] of Object.entries(config.entities)) for (const field of nonCanonicalFieldKeys(entity)) issues.push({
170
+ path: `entities.${key}.fields.${field}`,
171
+ message: nonCanonicalMessage(entity, field)
172
+ });
173
+ return issues;
174
+ }
175
+ function defaultWarn(message) {
176
+ console.warn(message);
177
+ }
149
178
  /**
150
179
  * Structural helper for `clivly.config.ts`. Gives full type inference on the
151
180
  * literal you pass and eagerly parses it, so shape errors surface at config
152
- * load rather than at sync time. Does NOT validate against the host schema —
153
- * that needs the discovered schema; see `validateEntitiesConfig`.
181
+ * load rather than at sync time.
182
+ *
183
+ * It also catches the silent-data-loss footgun: a non-canonical field key for a
184
+ * materialised concept (e.g. `fullName` instead of `name` on a `contact`) has no
185
+ * destination column, so the store would write an empty value. By default these
186
+ * are reported via `onWarn` (a `console.warn`); pass `{ strict: true }` to throw
187
+ * a `ClivlyConfigError` instead. Does NOT validate against the host schema — that
188
+ * needs the discovered schema; see `validateEntitiesConfig`.
154
189
  */
155
- function defineClivlyConfig(config) {
156
- return entitiesConfigSchema.parse(config);
190
+ function defineClivlyConfig(config, options) {
191
+ const parsed = entitiesConfigSchema.parse(config);
192
+ const issues = canonicalFieldIssues(parsed);
193
+ if (issues.length > 0) {
194
+ if (options?.strict) throw new ClivlyConfigError(issues);
195
+ const warn = options?.onWarn ?? defaultWarn;
196
+ for (const issue of issues) warn(`⚠ Clivly config: ${issue.message} (${issue.path}) — this field would write an empty value at sync time. Pass { strict: true } to make this throw.`);
197
+ }
198
+ return parsed;
157
199
  }
158
200
  /** Split a `table.column` / bare `column` ref into its parts. */
159
201
  function parseColumnRef(ref) {
@@ -191,11 +233,8 @@ var SchemaValidator = class {
191
233
  }
192
234
  };
193
235
  function validateFields(v, base, entity) {
194
- const canonical = CANONICAL_FIELDS[entity.concept];
195
- for (const [field, column] of Object.entries(entity.fields)) {
196
- v.requireColumn(`${base}.fields.${field}`, entity.source, column);
197
- if (canonical && !canonical.includes(field)) v.report(`${base}.fields.${field}`, `"${field}" is not a canonical ${entity.concept} field (expected one of: ${canonical.join(", ")})`);
198
- }
236
+ for (const [field, column] of Object.entries(entity.fields)) v.requireColumn(`${base}.fields.${field}`, entity.source, column);
237
+ for (const field of nonCanonicalFieldKeys(entity)) v.report(`${base}.fields.${field}`, nonCanonicalMessage(entity, field));
199
238
  }
200
239
  function validateReadOnly(v, base, entity) {
201
240
  for (const field of entity.readOnly ?? []) if (!(field in entity.fields)) v.report(`${base}.readOnly`, `"${field}" is not one of this entity's fields`);
@@ -263,4 +302,4 @@ function validateEntitiesConfig(config, schema) {
263
302
  };
264
303
  }
265
304
  //#endregion
266
- export { CANONICAL_FIELDS, CRM_CONCEPTS, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
305
+ export { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };