@contember/bindx 0.1.5 → 0.1.8
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.
- package/dist/adapter/ContemberAdapter.d.ts +11 -14
- package/dist/adapter/ContemberAdapter.d.ts.map +1 -1
- package/dist/brand/ComponentBrand.d.ts +1 -24
- package/dist/brand/ComponentBrand.d.ts.map +1 -1
- package/dist/contember/index.d.ts +1 -2
- package/dist/contember/index.d.ts.map +1 -1
- package/dist/handles/EntityHandle.d.ts +18 -29
- package/dist/handles/EntityHandle.d.ts.map +1 -1
- package/dist/handles/FieldHandle.d.ts +5 -3
- package/dist/handles/FieldHandle.d.ts.map +1 -1
- package/dist/handles/HasManyListHandle.d.ts +4 -3
- package/dist/handles/HasManyListHandle.d.ts.map +1 -1
- package/dist/handles/HasOneHandle.d.ts +33 -36
- package/dist/handles/HasOneHandle.d.ts.map +1 -1
- package/dist/handles/PlaceholderHandle.d.ts +9 -19
- package/dist/handles/PlaceholderHandle.d.ts.map +1 -1
- package/dist/handles/proxyFactory.d.ts +9 -37
- package/dist/handles/proxyFactory.d.ts.map +1 -1
- package/dist/handles/types.d.ts +4 -0
- package/dist/handles/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/persistence/ContemberSchemaMutationAdapter.d.ts +24 -1
- package/dist/persistence/ContemberSchemaMutationAdapter.d.ts.map +1 -1
- package/dist/schema/ContemberSchema.d.ts +1 -97
- package/dist/schema/ContemberSchema.d.ts.map +1 -1
- package/dist/schema/SchemaLoader.d.ts +1 -27
- package/dist/schema/SchemaLoader.d.ts.map +1 -1
- package/dist/schema/SchemaRegistry.d.ts +1 -94
- package/dist/schema/SchemaRegistry.d.ts.map +1 -1
- package/dist/schema/roles.d.ts +1 -61
- package/dist/schema/roles.d.ts.map +1 -1
- package/dist/schema/types.d.ts +1 -178
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/selection/SelectionMetaCollector.d.ts +1 -31
- package/dist/selection/SelectionMetaCollector.d.ts.map +1 -1
- package/dist/selection/SelectionScope.d.ts +1 -76
- package/dist/selection/SelectionScope.d.ts.map +1 -1
- package/dist/selection/buildQuery.d.ts +1 -44
- package/dist/selection/buildQuery.d.ts.map +1 -1
- package/dist/selection/createFragment.d.ts +1 -28
- package/dist/selection/createFragment.d.ts.map +1 -1
- package/dist/selection/createSelectionBuilder.d.ts +1 -17
- package/dist/selection/createSelectionBuilder.d.ts.map +1 -1
- package/dist/selection/queryTypes.d.ts +1 -83
- package/dist/selection/queryTypes.d.ts.map +1 -1
- package/dist/selection/types.d.ts +1 -237
- package/dist/selection/types.d.ts.map +1 -1
- package/dist/utils/aliasGenerator.d.ts +1 -24
- package/dist/utils/aliasGenerator.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/adapter/ContemberAdapter.ts +193 -123
- package/src/brand/ComponentBrand.ts +1 -28
- package/src/contember/index.ts +1 -8
- package/src/handles/EntityHandle.ts +131 -61
- package/src/handles/FieldHandle.ts +19 -3
- package/src/handles/HasManyListHandle.ts +15 -12
- package/src/handles/HasOneHandle.ts +98 -66
- package/src/handles/PlaceholderHandle.ts +26 -24
- package/src/handles/proxyFactory.ts +29 -128
- package/src/handles/types.ts +6 -0
- package/src/index.ts +22 -3
- package/src/persistence/ContemberSchemaMutationAdapter.ts +19 -1
- package/src/schema/ContemberSchema.ts +14 -132
- package/src/schema/SchemaLoader.ts +1 -93
- package/src/schema/SchemaRegistry.ts +1 -286
- package/src/schema/roles.ts +10 -83
- package/src/schema/types.ts +21 -238
- package/src/selection/SelectionMetaCollector.ts +1 -122
- package/src/selection/SelectionScope.ts +1 -208
- package/src/selection/buildQuery.ts +1 -134
- package/src/selection/createFragment.ts +1 -50
- package/src/selection/createSelectionBuilder.ts +1 -283
- package/src/selection/queryTypes.ts +8 -135
- package/src/selection/types.ts +13 -320
- package/src/utils/aliasGenerator.ts +1 -56
|
@@ -1,132 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
type
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface SchemaRelationOrderBy {
|
|
18
|
-
path: string[]
|
|
19
|
-
direction: 'asc' | 'desc'
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface BaseRelation {
|
|
23
|
-
__typename: '_Relation'
|
|
24
|
-
name: string
|
|
25
|
-
nullable: boolean | null
|
|
26
|
-
onDelete: 'restrict' | 'cascade' | 'setNull' | null
|
|
27
|
-
orderBy: SchemaRelationOrderBy[] | null
|
|
28
|
-
orphanRemoval: boolean | null
|
|
29
|
-
targetEntity: string
|
|
30
|
-
type: 'OneHasOne' | 'OneHasMany' | 'ManyHasOne' | 'ManyHasMany'
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface OwningRelation extends BaseRelation {
|
|
34
|
-
side: 'owning'
|
|
35
|
-
inversedBy: string | null
|
|
36
|
-
ownedBy?: never
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface InverseRelation extends BaseRelation {
|
|
40
|
-
side: 'inverse'
|
|
41
|
-
ownedBy: string
|
|
42
|
-
inversedBy?: never
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export type SchemaRelation = OwningRelation | InverseRelation
|
|
46
|
-
|
|
47
|
-
export type SchemaField = SchemaColumn | SchemaRelation
|
|
48
|
-
|
|
49
|
-
export interface SchemaEntity {
|
|
50
|
-
name: string
|
|
51
|
-
customPrimaryAllowed: boolean
|
|
52
|
-
fields: Map<string, SchemaField>
|
|
53
|
-
unique: { fields: Set<string> }[]
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface SchemaEnum {
|
|
57
|
-
name: string
|
|
58
|
-
values: string[]
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Processed schema store with Maps for efficient lookup.
|
|
63
|
-
* This is the format used internally after loading from API.
|
|
64
|
-
*/
|
|
65
|
-
export interface ContemberSchemaStore {
|
|
66
|
-
entities: Map<string, SchemaEntity>
|
|
67
|
-
enums: Map<string, Set<string>>
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Raw schema format as returned by the Contember API.
|
|
72
|
-
*/
|
|
73
|
-
export interface RawContemberSchema {
|
|
74
|
-
enums: { name: string; values: string[] }[]
|
|
75
|
-
entities: {
|
|
76
|
-
name: string
|
|
77
|
-
customPrimaryAllowed: boolean
|
|
78
|
-
fields: (SchemaColumn | (Omit<SchemaRelation, 'side'> & { side: 'owning' | 'inverse'; ownedBy?: string; inversedBy?: string | null }))[]
|
|
79
|
-
unique: { fields: string[] }[]
|
|
80
|
-
}[]
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Schema class compatible with binding-common's Schema.
|
|
85
|
-
* Can be used directly or created from API response.
|
|
86
|
-
*/
|
|
87
|
-
export class ContemberSchema {
|
|
88
|
-
constructor(private readonly store: ContemberSchemaStore) {}
|
|
89
|
-
|
|
90
|
-
getEntity(entityName: string): SchemaEntity | undefined {
|
|
91
|
-
return this.store.entities.get(entityName)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
getEntityNames(): string[] {
|
|
95
|
-
return Array.from(this.store.entities.keys())
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
getEntityField(entityName: string, fieldName: string): SchemaField | undefined {
|
|
99
|
-
return this.store.entities.get(entityName)?.fields.get(fieldName)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
getEnumValues(enumName: string): string[] | undefined {
|
|
103
|
-
const values = this.store.enums.get(enumName)
|
|
104
|
-
return values ? Array.from(values) : undefined
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Creates a ContemberSchema from raw API response.
|
|
109
|
-
*/
|
|
110
|
-
static fromRaw(raw: RawContemberSchema): ContemberSchema {
|
|
111
|
-
const enums = new Map<string, Set<string>>()
|
|
112
|
-
for (const { name, values } of raw.enums) {
|
|
113
|
-
enums.set(name, new Set(values))
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const entities = new Map<string, SchemaEntity>()
|
|
117
|
-
for (const entity of raw.entities) {
|
|
118
|
-
const fields = new Map<string, SchemaField>()
|
|
119
|
-
for (const field of entity.fields) {
|
|
120
|
-
fields.set(field.name, field as SchemaField)
|
|
121
|
-
}
|
|
122
|
-
entities.set(entity.name, {
|
|
123
|
-
name: entity.name,
|
|
124
|
-
customPrimaryAllowed: entity.customPrimaryAllowed,
|
|
125
|
-
fields,
|
|
126
|
-
unique: entity.unique.map(u => ({ fields: new Set(u.fields) })),
|
|
127
|
-
})
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return new ContemberSchema({ entities, enums })
|
|
131
|
-
}
|
|
132
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
ContemberSchema,
|
|
3
|
+
type SchemaColumnType,
|
|
4
|
+
type SchemaColumn,
|
|
5
|
+
type SchemaRelationOrderBy,
|
|
6
|
+
type OwningRelation,
|
|
7
|
+
type InverseRelation,
|
|
8
|
+
type SchemaRelation,
|
|
9
|
+
type SchemaField,
|
|
10
|
+
type SchemaEntity,
|
|
11
|
+
type SchemaEnum,
|
|
12
|
+
type ContemberSchemaStore,
|
|
13
|
+
type RawContemberSchema,
|
|
14
|
+
} from '@contember/bindx-client'
|
|
@@ -1,93 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Loads schema from Contember Content API via GraphQL introspection.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { ContemberSchema, type RawContemberSchema } from './ContemberSchema.js'
|
|
6
|
-
|
|
7
|
-
export interface SchemaLoaderClient {
|
|
8
|
-
execute<T>(query: string, options?: unknown): Promise<T>
|
|
9
|
-
apiUrl?: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const SCHEMA_QUERY = `query {
|
|
13
|
-
schema {
|
|
14
|
-
enums {
|
|
15
|
-
name
|
|
16
|
-
values
|
|
17
|
-
}
|
|
18
|
-
entities {
|
|
19
|
-
name
|
|
20
|
-
customPrimaryAllowed
|
|
21
|
-
unique {
|
|
22
|
-
fields
|
|
23
|
-
}
|
|
24
|
-
fields {
|
|
25
|
-
__typename
|
|
26
|
-
name
|
|
27
|
-
type
|
|
28
|
-
nullable
|
|
29
|
-
... on _Column {
|
|
30
|
-
enumName
|
|
31
|
-
defaultValue
|
|
32
|
-
}
|
|
33
|
-
... on _Relation {
|
|
34
|
-
side
|
|
35
|
-
targetEntity
|
|
36
|
-
ownedBy
|
|
37
|
-
inversedBy
|
|
38
|
-
onDelete
|
|
39
|
-
orphanRemoval
|
|
40
|
-
orderBy {
|
|
41
|
-
path
|
|
42
|
-
direction
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}`
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Loads and caches schema from Contember Content API.
|
|
52
|
-
* Instance-level cache avoids cross-tenant collisions and test pollution.
|
|
53
|
-
*/
|
|
54
|
-
export class SchemaLoader {
|
|
55
|
-
private readonly cache = new Map<string, Promise<ContemberSchema>>()
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Loads schema from the API, with caching per API URL.
|
|
59
|
-
*/
|
|
60
|
-
async loadSchema(
|
|
61
|
-
client: SchemaLoaderClient,
|
|
62
|
-
options?: unknown,
|
|
63
|
-
): Promise<ContemberSchema> {
|
|
64
|
-
const cacheKey = client.apiUrl ?? 'default'
|
|
65
|
-
|
|
66
|
-
const existing = this.cache.get(cacheKey)
|
|
67
|
-
if (existing) {
|
|
68
|
-
return existing
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const promise = (async () => {
|
|
72
|
-
const response = await client.execute<{ schema: RawContemberSchema }>(SCHEMA_QUERY, options)
|
|
73
|
-
return ContemberSchema.fromRaw(response.schema)
|
|
74
|
-
})()
|
|
75
|
-
|
|
76
|
-
this.cache.set(cacheKey, promise)
|
|
77
|
-
return promise
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Clears the schema cache.
|
|
82
|
-
*/
|
|
83
|
-
clearCache(): void {
|
|
84
|
-
this.cache.clear()
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Clears cache for a specific API URL.
|
|
89
|
-
*/
|
|
90
|
-
clearCacheFor(apiUrl: string): void {
|
|
91
|
-
this.cache.delete(apiUrl)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
1
|
+
export { SchemaLoader, type SchemaLoaderClient } from '@contember/bindx-client'
|
|
@@ -1,286 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type {
|
|
3
|
-
EntitySchemaDef,
|
|
4
|
-
FieldDef,
|
|
5
|
-
HasManyRelationDef,
|
|
6
|
-
HasOneRelationDef,
|
|
7
|
-
RelationType,
|
|
8
|
-
SchemaDefinition,
|
|
9
|
-
} from './types.js'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Runtime registry for entity schemas.
|
|
13
|
-
* Provides lookups for entity definitions, relation targets, and field types.
|
|
14
|
-
*/
|
|
15
|
-
export class SchemaRegistry<TModels extends Record<string, object> = Record<string, object>> {
|
|
16
|
-
private readonly entityDefs: Map<string, EntitySchemaDef>
|
|
17
|
-
|
|
18
|
-
constructor(definition: SchemaDefinition<TModels>) {
|
|
19
|
-
this.entityDefs = new Map()
|
|
20
|
-
|
|
21
|
-
for (const [entityName, entityDef] of Object.entries(definition.entities)) {
|
|
22
|
-
this.entityDefs.set(entityName, normalizeEntityDef(entityDef as EntitySchemaDef))
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Creates a SchemaRegistry from a ContemberSchema (loaded from API or injected).
|
|
28
|
-
* This is the preferred way to create a SchemaRegistry at runtime.
|
|
29
|
-
*/
|
|
30
|
-
static fromContemberSchema(schema: ContemberSchema): SchemaRegistry {
|
|
31
|
-
const entities: Record<string, EntitySchemaDef> = {}
|
|
32
|
-
|
|
33
|
-
for (const entityName of schema.getEntityNames()) {
|
|
34
|
-
const entity = schema.getEntity(entityName)
|
|
35
|
-
if (!entity) continue
|
|
36
|
-
|
|
37
|
-
const fields: Record<string, FieldDef> = {}
|
|
38
|
-
|
|
39
|
-
for (const [fieldName, field] of entity.fields) {
|
|
40
|
-
if (field.__typename === '_Column') {
|
|
41
|
-
fields[fieldName] = { type: 'scalar' }
|
|
42
|
-
} else if (field.__typename === '_Relation') {
|
|
43
|
-
const isMany = field.type === 'OneHasMany' || field.type === 'ManyHasMany'
|
|
44
|
-
if (isMany) {
|
|
45
|
-
fields[fieldName] = {
|
|
46
|
-
type: 'hasMany',
|
|
47
|
-
target: field.targetEntity,
|
|
48
|
-
inverse: field.side === 'owning' ? field.inversedBy ?? undefined : field.ownedBy,
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
fields[fieldName] = {
|
|
52
|
-
type: 'hasOne',
|
|
53
|
-
target: field.targetEntity,
|
|
54
|
-
inverse: field.side === 'owning' ? field.inversedBy ?? undefined : field.ownedBy,
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
entities[entityName] = { fields }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return new SchemaRegistry({ entities } as SchemaDefinition<Record<string, object>>)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Gets the entity names defined in the schema
|
|
68
|
-
*/
|
|
69
|
-
getEntityNames(): string[] {
|
|
70
|
-
return Array.from(this.entityDefs.keys())
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Checks if an entity type exists in the schema
|
|
75
|
-
*/
|
|
76
|
-
hasEntity(entityType: string): boolean {
|
|
77
|
-
return this.entityDefs.has(entityType)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Gets the schema definition for an entity type
|
|
82
|
-
*/
|
|
83
|
-
getEntityDef(entityType: string): EntitySchemaDef | undefined {
|
|
84
|
-
return this.entityDefs.get(entityType)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Gets the field definition for a specific field on an entity
|
|
89
|
-
*/
|
|
90
|
-
getFieldDef(entityType: string, fieldName: string): FieldDef | undefined {
|
|
91
|
-
const entityDef = this.entityDefs.get(entityType)
|
|
92
|
-
if (!entityDef) return undefined
|
|
93
|
-
return entityDef.fields[fieldName]
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Gets the enum values for an enum field.
|
|
98
|
-
* Returns undefined if the field is not an enum or doesn't exist.
|
|
99
|
-
*/
|
|
100
|
-
getEnumValues(entityType: string, fieldName: string): readonly string[] | undefined {
|
|
101
|
-
const fieldDef = this.getFieldDef(entityType, fieldName)
|
|
102
|
-
if (!fieldDef || fieldDef.type !== 'enum') return undefined
|
|
103
|
-
return fieldDef.values
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Gets the enum name for an enum field.
|
|
108
|
-
* Returns undefined if the field is not an enum or doesn't exist.
|
|
109
|
-
*/
|
|
110
|
-
getEnumName(entityType: string, fieldName: string): string | undefined {
|
|
111
|
-
const fieldDef = this.getFieldDef(entityType, fieldName)
|
|
112
|
-
if (!fieldDef || fieldDef.type !== 'enum') return undefined
|
|
113
|
-
return fieldDef.enumName
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Gets the column type for a scalar field (e.g. 'String', 'Integer', 'Date').
|
|
118
|
-
* Returns undefined if the field is not a scalar or doesn't exist.
|
|
119
|
-
*/
|
|
120
|
-
getColumnType(entityType: string, fieldName: string): string | undefined {
|
|
121
|
-
const fieldDef = this.getFieldDef(entityType, fieldName)
|
|
122
|
-
if (!fieldDef || fieldDef.type !== 'scalar') return undefined
|
|
123
|
-
return fieldDef.columnType
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Gets the target entity type for a relation field.
|
|
128
|
-
* Returns undefined if the field is not a relation or doesn't exist.
|
|
129
|
-
*/
|
|
130
|
-
getRelationTarget(entityType: string, fieldName: string): string | undefined {
|
|
131
|
-
const fieldDef = this.getFieldDef(entityType, fieldName)
|
|
132
|
-
if (!fieldDef) return undefined
|
|
133
|
-
|
|
134
|
-
if (fieldDef.type === 'hasOne' || fieldDef.type === 'hasMany') {
|
|
135
|
-
return fieldDef.target
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return undefined
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Gets the relation type for a field (hasOne or hasMany).
|
|
143
|
-
* Returns undefined if the field is not a relation.
|
|
144
|
-
*/
|
|
145
|
-
getRelationType(entityType: string, fieldName: string): RelationType | undefined {
|
|
146
|
-
const fieldDef = this.getFieldDef(entityType, fieldName)
|
|
147
|
-
if (!fieldDef) return undefined
|
|
148
|
-
|
|
149
|
-
if (fieldDef.type === 'hasOne' || fieldDef.type === 'hasMany') {
|
|
150
|
-
return fieldDef.type
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return undefined
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Checks if a field is a relation (hasOne or hasMany)
|
|
158
|
-
*/
|
|
159
|
-
isRelation(entityType: string, fieldName: string): boolean {
|
|
160
|
-
return this.getRelationType(entityType, fieldName) !== undefined
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Checks if a field is a has-one relation
|
|
165
|
-
*/
|
|
166
|
-
isHasOne(entityType: string, fieldName: string): boolean {
|
|
167
|
-
return this.getRelationType(entityType, fieldName) === 'hasOne'
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Checks if a field is a has-many relation
|
|
172
|
-
*/
|
|
173
|
-
isHasMany(entityType: string, fieldName: string): boolean {
|
|
174
|
-
return this.getRelationType(entityType, fieldName) === 'hasMany'
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Checks if a field is a scalar (non-relation) field
|
|
179
|
-
*/
|
|
180
|
-
isScalar(entityType: string, fieldName: string): boolean {
|
|
181
|
-
const fieldDef = this.getFieldDef(entityType, fieldName)
|
|
182
|
-
return fieldDef?.type === 'scalar'
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Gets the inverse field name for a relation.
|
|
187
|
-
* Returns undefined if no inverse is defined.
|
|
188
|
-
*/
|
|
189
|
-
getInverseField(entityType: string, fieldName: string): string | undefined {
|
|
190
|
-
const fieldDef = this.getFieldDef(entityType, fieldName)
|
|
191
|
-
if (!fieldDef) return undefined
|
|
192
|
-
if (fieldDef.type === 'scalar') return undefined
|
|
193
|
-
|
|
194
|
-
return (fieldDef as HasOneRelationDef | HasManyRelationDef).inverse
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Gets all scalar field names for an entity
|
|
199
|
-
*/
|
|
200
|
-
getScalarFields(entityType: string): string[] {
|
|
201
|
-
const entityDef = this.entityDefs.get(entityType)
|
|
202
|
-
if (!entityDef) return []
|
|
203
|
-
|
|
204
|
-
return Object.entries(entityDef.fields)
|
|
205
|
-
.filter(([_, def]) => def.type === 'scalar')
|
|
206
|
-
.map(([name]) => name)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Gets all relation field names for an entity
|
|
211
|
-
*/
|
|
212
|
-
getRelationFields(entityType: string): string[] {
|
|
213
|
-
const entityDef = this.entityDefs.get(entityType)
|
|
214
|
-
if (!entityDef) return []
|
|
215
|
-
|
|
216
|
-
return Object.entries(entityDef.fields)
|
|
217
|
-
.filter(([_, def]) => def.type === 'hasOne' || def.type === 'hasMany')
|
|
218
|
-
.map(([name]) => name)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Gets all field names for an entity
|
|
223
|
-
*/
|
|
224
|
-
getAllFields(entityType: string): string[] {
|
|
225
|
-
const entityDef = this.entityDefs.get(entityType)
|
|
226
|
-
if (!entityDef) return []
|
|
227
|
-
|
|
228
|
-
return Object.keys(entityDef.fields)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Validates that all relation targets exist in the schema.
|
|
233
|
-
* Throws an error if any target is missing.
|
|
234
|
-
*/
|
|
235
|
-
validate(): void {
|
|
236
|
-
for (const [entityName, entityDef] of this.entityDefs) {
|
|
237
|
-
for (const [fieldName, fieldDef] of Object.entries(entityDef.fields)) {
|
|
238
|
-
if (fieldDef.type === 'hasOne' || fieldDef.type === 'hasMany') {
|
|
239
|
-
if (!this.entityDefs.has(fieldDef.target)) {
|
|
240
|
-
throw new Error(
|
|
241
|
-
`Invalid schema: Entity "${entityName}" field "${fieldName}" references ` +
|
|
242
|
-
`unknown entity "${fieldDef.target}"`,
|
|
243
|
-
)
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Normalizes field definitions from Contember SchemaNames format
|
|
253
|
-
* (column/one/many with entity) to bindx FieldDef format (scalar/hasOne/hasMany with target).
|
|
254
|
-
*/
|
|
255
|
-
function normalizeEntityDef(entityDef: EntitySchemaDef): EntitySchemaDef {
|
|
256
|
-
const fields: Record<string, FieldDef> = {}
|
|
257
|
-
let needsNormalization = false
|
|
258
|
-
|
|
259
|
-
for (const [fieldName, fieldDef] of Object.entries(entityDef.fields)) {
|
|
260
|
-
const raw = fieldDef as unknown as Record<string, unknown>
|
|
261
|
-
const type = raw['type'] as string
|
|
262
|
-
|
|
263
|
-
if (type === 'column') {
|
|
264
|
-
fields[fieldName] = { type: 'scalar' }
|
|
265
|
-
needsNormalization = true
|
|
266
|
-
} else if (type === 'one') {
|
|
267
|
-
fields[fieldName] = {
|
|
268
|
-
type: 'hasOne',
|
|
269
|
-
target: (raw['entity'] as string) ?? (raw['target'] as string),
|
|
270
|
-
inverse: raw['inverse'] as string | undefined,
|
|
271
|
-
}
|
|
272
|
-
needsNormalization = true
|
|
273
|
-
} else if (type === 'many') {
|
|
274
|
-
fields[fieldName] = {
|
|
275
|
-
type: 'hasMany',
|
|
276
|
-
target: (raw['entity'] as string) ?? (raw['target'] as string),
|
|
277
|
-
inverse: raw['inverse'] as string | undefined,
|
|
278
|
-
}
|
|
279
|
-
needsNormalization = true
|
|
280
|
-
} else {
|
|
281
|
-
fields[fieldName] = fieldDef
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return needsNormalization ? { fields } : entityDef
|
|
286
|
-
}
|
|
1
|
+
export { SchemaRegistry } from '@contember/bindx-client'
|
package/src/schema/roles.ts
CHANGED
|
@@ -1,83 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// ============================================================================
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Converts a union type to an intersection type.
|
|
15
|
-
* Used to merge field sets from multiple roles.
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* UnionToIntersection<{ a: 1 } | { b: 2 }> = { a: 1 } & { b: 2 }
|
|
19
|
-
*/
|
|
20
|
-
export type UnionToIntersection<U> =
|
|
21
|
-
(U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never
|
|
22
|
-
|
|
23
|
-
// ============================================================================
|
|
24
|
-
// Role Map Types
|
|
25
|
-
// ============================================================================
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Extracts all role names from a role map.
|
|
29
|
-
*/
|
|
30
|
-
export type RoleNames<TRoleMap extends Record<string, object>> = keyof TRoleMap & string
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Default entity type — the narrowest scope.
|
|
34
|
-
* Returns a union of all role entity types, so only common fields are accessible via keyof.
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* CommonEntity<{ admin: { id: string; secret: string }; public: { id: string } }>
|
|
38
|
-
* // = { id: string; secret: string } | { id: string }
|
|
39
|
-
* // Only `id` is accessible (common to all roles)
|
|
40
|
-
*/
|
|
41
|
-
export type CommonEntity<TRoleMap extends Record<string, object>> = TRoleMap[keyof TRoleMap]
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Entity type for specific roles — expanded scope.
|
|
45
|
-
* Returns an intersection of the specified roles' entity types,
|
|
46
|
-
* giving access to all fields from any of those roles.
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* EntityForRoles<{ admin: { id: string; secret: string }; editor: { id: string; notes: string } }, 'admin'>
|
|
50
|
-
* // = { id: string; secret: string }
|
|
51
|
-
*
|
|
52
|
-
* EntityForRoles<{ admin: { id: string; secret: string }; editor: { id: string; notes: string } }, 'admin' | 'editor'>
|
|
53
|
-
* // = { id: string; secret: string } & { id: string; notes: string }
|
|
54
|
-
* // = { id: string; secret: string; notes: string }
|
|
55
|
-
*/
|
|
56
|
-
export type EntityForRoles<
|
|
57
|
-
TRoleMap extends Record<string, object>,
|
|
58
|
-
TRoles extends keyof TRoleMap,
|
|
59
|
-
> = UnionToIntersection<TRoleMap[TRoles]> & object
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Resolves the entity type based on whether roles are specified.
|
|
63
|
-
* - No roles (TRoles = never): returns CommonEntity (narrowest)
|
|
64
|
-
* - With roles: returns EntityForRoles (expanded)
|
|
65
|
-
*/
|
|
66
|
-
export type ResolveEntity<
|
|
67
|
-
TRoleMap extends Record<string, object>,
|
|
68
|
-
TRoles extends string,
|
|
69
|
-
> = [TRoles] extends [never]
|
|
70
|
-
? CommonEntity<TRoleMap>
|
|
71
|
-
: EntityForRoles<TRoleMap, TRoles & keyof TRoleMap>
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Sentinel role name used when no ACL roles are defined.
|
|
75
|
-
* entityDef<Article>('Article') wraps as { _default: Article }.
|
|
76
|
-
*/
|
|
77
|
-
export const DEFAULT_ROLE = '_default' as const
|
|
78
|
-
export type DefaultRole = typeof DEFAULT_ROLE
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Wraps a single entity type as a single-role map with the _default role.
|
|
82
|
-
*/
|
|
83
|
-
export type SingleRoleMap<TEntity extends object> = { readonly [K in DefaultRole]: TEntity }
|
|
1
|
+
export {
|
|
2
|
+
type UnionToIntersection,
|
|
3
|
+
type RoleNames,
|
|
4
|
+
type CommonEntity,
|
|
5
|
+
type EntityForRoles,
|
|
6
|
+
type ResolveEntity,
|
|
7
|
+
type SingleRoleMap,
|
|
8
|
+
type DefaultRole,
|
|
9
|
+
DEFAULT_ROLE,
|
|
10
|
+
} from '@contember/bindx-client'
|