@contember/bindx-generator 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.
- package/README.md +258 -0
- package/dist/BindxGenerator.d.ts +41 -0
- package/dist/BindxGenerator.d.ts.map +1 -0
- package/dist/BindxGenerator.js +124 -0
- package/dist/BindxGenerator.js.map +1 -0
- package/dist/EntityTypeSchemaGenerator.d.ts +13 -0
- package/dist/EntityTypeSchemaGenerator.d.ts.map +1 -0
- package/dist/EntityTypeSchemaGenerator.js +64 -0
- package/dist/EntityTypeSchemaGenerator.js.map +1 -0
- package/dist/EnumTypeSchemaGenerator.d.ts +9 -0
- package/dist/EnumTypeSchemaGenerator.d.ts.map +1 -0
- package/dist/EnumTypeSchemaGenerator.js +19 -0
- package/dist/EnumTypeSchemaGenerator.js.map +1 -0
- package/dist/NameSchemaGenerator.d.ts +34 -0
- package/dist/NameSchemaGenerator.d.ts.map +1 -0
- package/dist/NameSchemaGenerator.js +39 -0
- package/dist/NameSchemaGenerator.js.map +1 -0
- package/dist/RoleNameSchemaGenerator.d.ts +24 -0
- package/dist/RoleNameSchemaGenerator.d.ts.map +1 -0
- package/dist/RoleNameSchemaGenerator.js +194 -0
- package/dist/RoleNameSchemaGenerator.js.map +1 -0
- package/dist/RoleSchemaGenerator.d.ts +41 -0
- package/dist/RoleSchemaGenerator.d.ts.map +1 -0
- package/dist/RoleSchemaGenerator.js +169 -0
- package/dist/RoleSchemaGenerator.js.map +1 -0
- package/dist/generate.d.ts +12 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +45 -0
- package/dist/generate.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/utils.d.ts +17 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +51 -0
- package/dist/utils.js.map +1 -0
- package/package.json +31 -0
- package/src/BindxGenerator.ts +154 -0
- package/src/EntityTypeSchemaGenerator.ts +77 -0
- package/src/EnumTypeSchemaGenerator.ts +24 -0
- package/src/NameSchemaGenerator.ts +66 -0
- package/src/RoleSchemaGenerator.ts +219 -0
- package/src/generate.ts +55 -0
- package/src/index.ts +19 -0
- package/src/utils.ts +54 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main bindx schema generator
|
|
3
|
+
*
|
|
4
|
+
* Generates TypeScript schema files from Contember Model.Schema.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Model, Acl } from '@contember/schema'
|
|
8
|
+
import { EntityTypeSchemaGenerator } from './EntityTypeSchemaGenerator'
|
|
9
|
+
import { EnumTypeSchemaGenerator } from './EnumTypeSchemaGenerator'
|
|
10
|
+
import { NameSchemaGenerator } from './NameSchemaGenerator'
|
|
11
|
+
import { RoleSchemaGenerator, type RoleSchemaGeneratorOptions } from './RoleSchemaGenerator'
|
|
12
|
+
|
|
13
|
+
export interface BindxGeneratorOptions extends RoleSchemaGeneratorOptions {
|
|
14
|
+
// Reserved for future options
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface GeneratedFiles {
|
|
18
|
+
'entities.ts': string
|
|
19
|
+
'names.ts'?: string
|
|
20
|
+
'enums.ts': string
|
|
21
|
+
'types.ts': string
|
|
22
|
+
'schema.ts': string
|
|
23
|
+
'index.ts': string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class BindxGenerator {
|
|
27
|
+
private readonly entityTypeSchemaGenerator: EntityTypeSchemaGenerator
|
|
28
|
+
private readonly enumTypeSchemaGenerator: EnumTypeSchemaGenerator
|
|
29
|
+
private readonly nameSchemaGenerator: NameSchemaGenerator
|
|
30
|
+
private readonly roleSchemaGenerator: RoleSchemaGenerator
|
|
31
|
+
|
|
32
|
+
constructor(private readonly options: BindxGeneratorOptions = {}) {
|
|
33
|
+
this.entityTypeSchemaGenerator = new EntityTypeSchemaGenerator()
|
|
34
|
+
this.enumTypeSchemaGenerator = new EnumTypeSchemaGenerator()
|
|
35
|
+
this.nameSchemaGenerator = new NameSchemaGenerator()
|
|
36
|
+
this.roleSchemaGenerator = new RoleSchemaGenerator(options)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate schema files
|
|
41
|
+
*/
|
|
42
|
+
generate(model: Model.Schema, acl?: Acl.Schema): GeneratedFiles {
|
|
43
|
+
const enumsCode = this.enumTypeSchemaGenerator.generate(model)
|
|
44
|
+
let entitiesCode = this.entityTypeSchemaGenerator.generate(model)
|
|
45
|
+
const namesSchema = this.nameSchemaGenerator.generate(model)
|
|
46
|
+
|
|
47
|
+
// Append per-role entity types if ACL is provided
|
|
48
|
+
if (acl) {
|
|
49
|
+
entitiesCode += '\n' + this.roleSchemaGenerator.generateRoleEntities(model, acl)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const namesCode = `import type { BindxSchemaNames } from './types'
|
|
53
|
+
|
|
54
|
+
export const schemaNames: BindxSchemaNames = ${JSON.stringify(namesSchema, null, '\t')}
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
const typesCode = this.generateTypesFile()
|
|
58
|
+
const schemaCode = acl
|
|
59
|
+
? this.roleSchemaGenerator.generateSchemaFile(model, acl)
|
|
60
|
+
: this.generateSchemaFile(model)
|
|
61
|
+
|
|
62
|
+
const indexCode = `export * from './enums'
|
|
63
|
+
export * from './entities'
|
|
64
|
+
export * from './names'
|
|
65
|
+
export * from './types'
|
|
66
|
+
export * from './schema'
|
|
67
|
+
`
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
'entities.ts': entitiesCode,
|
|
71
|
+
'names.ts': namesCode,
|
|
72
|
+
'enums.ts': enumsCode,
|
|
73
|
+
'types.ts': typesCode,
|
|
74
|
+
'schema.ts': schemaCode,
|
|
75
|
+
'index.ts': indexCode,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private generateSchemaFile(model: Model.Schema): string {
|
|
80
|
+
const entityNames = Object.values(model.entities).map(e => e.name).sort()
|
|
81
|
+
|
|
82
|
+
const imports = entityNames.join(', ')
|
|
83
|
+
const entries = entityNames
|
|
84
|
+
.map(name => `\t${name}: entityDef<${name}>('${name}', schemaDef),`)
|
|
85
|
+
.join('\n')
|
|
86
|
+
|
|
87
|
+
return `import { entityDef } from '@contember/bindx'
|
|
88
|
+
import { schemaNamesToDef } from '@contember/bindx-react'
|
|
89
|
+
import type { ${imports} } from './entities'
|
|
90
|
+
import { schemaNames } from './names'
|
|
91
|
+
|
|
92
|
+
const schemaDef = schemaNamesToDef(schemaNames)
|
|
93
|
+
|
|
94
|
+
export const schema = {
|
|
95
|
+
${entries}
|
|
96
|
+
} as const
|
|
97
|
+
`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private generateTypesFile(): string {
|
|
101
|
+
return `/**
|
|
102
|
+
* Shared types for bindx schema
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
export interface BindxSchemaEntityNames {
|
|
106
|
+
readonly name: string
|
|
107
|
+
readonly scalars: readonly string[]
|
|
108
|
+
readonly fields: {
|
|
109
|
+
readonly [fieldName: string]:
|
|
110
|
+
| { readonly type: 'column'; readonly enumName?: string }
|
|
111
|
+
| { readonly type: 'one'; readonly entity: string }
|
|
112
|
+
| { readonly type: 'many'; readonly entity: string }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface BindxSchemaNames {
|
|
117
|
+
readonly entities: {
|
|
118
|
+
readonly [entityName: string]: BindxSchemaEntityNames
|
|
119
|
+
}
|
|
120
|
+
readonly enums: {
|
|
121
|
+
readonly [enumName: string]: readonly string[]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
`
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Generate bindx schema files from Contember model
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* const files = generate(model)
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export function generate(
|
|
137
|
+
model: Model.Schema,
|
|
138
|
+
aclOrOptions?: Acl.Schema | BindxGeneratorOptions,
|
|
139
|
+
options?: BindxGeneratorOptions,
|
|
140
|
+
): GeneratedFiles {
|
|
141
|
+
// Support both generate(model, acl, options) and generate(model, options)
|
|
142
|
+
let acl: Acl.Schema | undefined
|
|
143
|
+
let opts: BindxGeneratorOptions | undefined
|
|
144
|
+
|
|
145
|
+
if (aclOrOptions && 'roles' in aclOrOptions) {
|
|
146
|
+
acl = aclOrOptions
|
|
147
|
+
opts = options
|
|
148
|
+
} else {
|
|
149
|
+
opts = aclOrOptions as BindxGeneratorOptions | undefined
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const generator = new BindxGenerator(opts)
|
|
153
|
+
return generator.generate(model, acl)
|
|
154
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity type schema generator for bindx
|
|
3
|
+
* Generates TypeScript entity types from Contember model
|
|
4
|
+
*
|
|
5
|
+
* Output format is designed to work with bindx's type system,
|
|
6
|
+
* separating columns, hasOne, and hasMany for proper type inference.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Model } from '@contember/schema'
|
|
10
|
+
import { acceptEveryFieldVisitor } from '@contember/schema-utils'
|
|
11
|
+
import { columnToTsType, getEnumTypeName } from './utils'
|
|
12
|
+
|
|
13
|
+
export class EntityTypeSchemaGenerator {
|
|
14
|
+
generate(model: Model.Schema): string {
|
|
15
|
+
let code = ''
|
|
16
|
+
|
|
17
|
+
// Import enum types
|
|
18
|
+
for (const enumName of Object.keys(model.enums)) {
|
|
19
|
+
code += `import type { ${getEnumTypeName(enumName)} } from './enums'\n`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Add JSON type definitions
|
|
23
|
+
code += `
|
|
24
|
+
export type JSONPrimitive = string | number | boolean | null
|
|
25
|
+
export type JSONValue = JSONPrimitive | JSONObject | JSONArray
|
|
26
|
+
export type JSONObject = { readonly [K in string]?: JSONValue }
|
|
27
|
+
export type JSONArray = readonly JSONValue[]
|
|
28
|
+
|
|
29
|
+
`
|
|
30
|
+
|
|
31
|
+
// Generate entity types
|
|
32
|
+
for (const entity of Object.values(model.entities)) {
|
|
33
|
+
code += this.generateEntityTypeCode(model, entity)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Generate schema type
|
|
37
|
+
code += '\n'
|
|
38
|
+
code += `export interface BindxEntities {\n`
|
|
39
|
+
for (const entity of Object.values(model.entities)) {
|
|
40
|
+
code += `\t${entity.name}: ${entity.name}\n`
|
|
41
|
+
}
|
|
42
|
+
code += '}\n\n'
|
|
43
|
+
|
|
44
|
+
code += `export interface BindxSchema {\n`
|
|
45
|
+
code += '\tentities: BindxEntities\n'
|
|
46
|
+
code += '}\n'
|
|
47
|
+
|
|
48
|
+
return code
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private generateEntityTypeCode(model: Model.Schema, entity: Model.Entity): string {
|
|
52
|
+
let code = `export interface ${entity.name} {\n`
|
|
53
|
+
|
|
54
|
+
let columnsCode = ''
|
|
55
|
+
let hasOneCode = ''
|
|
56
|
+
let hasManyCode = ''
|
|
57
|
+
|
|
58
|
+
acceptEveryFieldVisitor(model, entity, {
|
|
59
|
+
visitHasMany: ctx => {
|
|
60
|
+
hasManyCode += `\t\t${ctx.relation.name}: ${ctx.targetEntity.name}[]\n`
|
|
61
|
+
},
|
|
62
|
+
visitHasOne: ctx => {
|
|
63
|
+
hasOneCode += `\t\t${ctx.relation.name}: ${ctx.targetEntity.name}\n`
|
|
64
|
+
},
|
|
65
|
+
visitColumn: ctx => {
|
|
66
|
+
columnsCode += `\t\t${ctx.column.name}: ${columnToTsType(ctx.column)}${ctx.column.nullable ? ' | null' : ''}\n`
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
code += columnsCode
|
|
71
|
+
code += hasOneCode
|
|
72
|
+
code += hasManyCode
|
|
73
|
+
code += '}\n\n'
|
|
74
|
+
|
|
75
|
+
return code
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enum type schema generator for bindx
|
|
3
|
+
* Generates TypeScript enum types from Contember model enums
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Model } from '@contember/schema'
|
|
7
|
+
import { getEnumTypeName } from './utils'
|
|
8
|
+
|
|
9
|
+
export class EnumTypeSchemaGenerator {
|
|
10
|
+
generate(model: Model.Schema): string {
|
|
11
|
+
let code = ''
|
|
12
|
+
|
|
13
|
+
for (const [enumName, values] of Object.entries(model.enums)) {
|
|
14
|
+
code += `export type ${getEnumTypeName(enumName)} = ${values.map(v => `'${v}'`).join(' | ')}\n\n`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Export enum values as const arrays for runtime use
|
|
18
|
+
for (const [enumName, values] of Object.entries(model.enums)) {
|
|
19
|
+
code += `export const ${enumName}Values = [${values.map(v => `'${v}'`).join(', ')}] as const\n`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return code
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name schema generator for bindx
|
|
3
|
+
* Generates runtime schema names (JSON structure) for query building
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Model } from '@contember/schema'
|
|
7
|
+
import { acceptEveryFieldVisitor } from '@contember/schema-utils'
|
|
8
|
+
|
|
9
|
+
export interface BindxSchemaEntityNames {
|
|
10
|
+
readonly name: string
|
|
11
|
+
readonly scalars: readonly string[]
|
|
12
|
+
readonly fields: {
|
|
13
|
+
readonly [fieldName: string]:
|
|
14
|
+
| { readonly type: 'column'; readonly columnType?: string; readonly enumName?: string }
|
|
15
|
+
| { readonly type: 'one'; readonly entity: string }
|
|
16
|
+
| { readonly type: 'many'; readonly entity: string }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BindxSchemaNames {
|
|
21
|
+
readonly entities: {
|
|
22
|
+
readonly [entityName: string]: BindxSchemaEntityNames
|
|
23
|
+
}
|
|
24
|
+
readonly enums: {
|
|
25
|
+
readonly [enumName: string]: readonly string[]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class NameSchemaGenerator {
|
|
30
|
+
generate(model: Model.Schema): BindxSchemaNames {
|
|
31
|
+
return {
|
|
32
|
+
entities: Object.fromEntries(
|
|
33
|
+
Object.values(model.entities).map(entity => {
|
|
34
|
+
const fields: Record<string, BindxSchemaEntityNames['fields'][string]> = {}
|
|
35
|
+
const scalars: string[] = []
|
|
36
|
+
|
|
37
|
+
acceptEveryFieldVisitor(model, entity, {
|
|
38
|
+
visitHasOne: ctx => {
|
|
39
|
+
fields[ctx.relation.name] = {
|
|
40
|
+
type: 'one',
|
|
41
|
+
entity: ctx.targetEntity.name,
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
visitHasMany: ctx => {
|
|
45
|
+
fields[ctx.relation.name] = {
|
|
46
|
+
type: 'many',
|
|
47
|
+
entity: ctx.targetEntity.name,
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
visitColumn: ctx => {
|
|
51
|
+
scalars.push(ctx.column.name)
|
|
52
|
+
fields[ctx.column.name] = ctx.column.type === Model.ColumnType.Enum
|
|
53
|
+
? { type: 'column', columnType: ctx.column.columnType, enumName: ctx.column.columnType }
|
|
54
|
+
: { type: 'column', columnType: ctx.column.columnType }
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return [entity.name, { name: entity.name, fields, scalars }]
|
|
59
|
+
}),
|
|
60
|
+
),
|
|
61
|
+
enums: Object.fromEntries(
|
|
62
|
+
Object.entries(model.enums).map(([name, values]) => [name, values]),
|
|
63
|
+
),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role-aware schema generator for bindx.
|
|
3
|
+
*
|
|
4
|
+
* Generates per-role entity interfaces filtered by Contember ACL read permissions.
|
|
5
|
+
* For each role, only fields with read access (true or predicate) are included.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Model, Acl } from '@contember/schema'
|
|
9
|
+
import { acceptEveryFieldVisitor } from '@contember/schema-utils'
|
|
10
|
+
import { columnToTsType, getEnumTypeName } from './utils'
|
|
11
|
+
|
|
12
|
+
export interface RoleSchemaGeneratorOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Whether to treat predicate-based permissions as allowed.
|
|
15
|
+
* When true, any non-false permission allows access.
|
|
16
|
+
* When false, only explicit `true` permissions are allowed.
|
|
17
|
+
* Default: true
|
|
18
|
+
*/
|
|
19
|
+
allowPredicateAccess?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface RoleFieldAccess {
|
|
23
|
+
/** role name → set of readable field names */
|
|
24
|
+
[role: string]: Set<string>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class RoleSchemaGenerator {
|
|
28
|
+
private readonly allowPredicateAccess: boolean
|
|
29
|
+
|
|
30
|
+
constructor(options: RoleSchemaGeneratorOptions = {}) {
|
|
31
|
+
this.allowPredicateAccess = options.allowPredicateAccess ?? true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generates per-role entity types + role map types.
|
|
36
|
+
* Returns code to be appended to entities.ts.
|
|
37
|
+
*/
|
|
38
|
+
generateRoleEntities(model: Model.Schema, acl: Acl.Schema): string {
|
|
39
|
+
const roles = this.resolveRoles(acl)
|
|
40
|
+
const roleNames = Object.keys(roles)
|
|
41
|
+
|
|
42
|
+
let code = ''
|
|
43
|
+
|
|
44
|
+
// Generate per-role entity interfaces
|
|
45
|
+
for (const roleName of roleNames) {
|
|
46
|
+
const roleAccess = roles[roleName]!
|
|
47
|
+
code += this.generateRoleEntityTypes(model, roleName, roleAccess)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Generate RoleSchemas type map per entity
|
|
51
|
+
for (const entity of Object.values(model.entities)) {
|
|
52
|
+
const roleEntries = roleNames
|
|
53
|
+
.filter(role => roles[role]!.has(entity.name))
|
|
54
|
+
.map(role => `\treadonly ${role}: ${this.roleEntityName(role, entity.name)}`)
|
|
55
|
+
.join('\n')
|
|
56
|
+
|
|
57
|
+
if (roleEntries) {
|
|
58
|
+
code += `export interface ${entity.name}$Roles {\n${roleEntries}\n}\n\n`
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Export available roles type
|
|
63
|
+
code += `export type AvailableRoles = ${roleNames.map(r => `'${r}'`).join(' | ')}\n`
|
|
64
|
+
|
|
65
|
+
return code
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generates schema.ts content using roleEntityDef.
|
|
70
|
+
*/
|
|
71
|
+
generateSchemaFile(model: Model.Schema, acl: Acl.Schema): string {
|
|
72
|
+
const roles = this.resolveRoles(acl)
|
|
73
|
+
const roleNames = Object.keys(roles)
|
|
74
|
+
const entityNames = Object.values(model.entities).map(e => e.name).sort()
|
|
75
|
+
|
|
76
|
+
// Build imports for role entity types
|
|
77
|
+
const roleTypeImports: string[] = []
|
|
78
|
+
for (const name of entityNames) {
|
|
79
|
+
const hasRoles = roleNames.some(role => roles[role]!.has(name))
|
|
80
|
+
if (hasRoles) {
|
|
81
|
+
roleTypeImports.push(`${name}$Roles`)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const entries = entityNames.map(name => {
|
|
86
|
+
const hasRoles = roleNames.some(role => roles[role]!.has(name))
|
|
87
|
+
if (hasRoles) {
|
|
88
|
+
return `\t${name}: roleEntityDef<${name}$Roles>('${name}', schemaDef),`
|
|
89
|
+
}
|
|
90
|
+
return `\t${name}: entityDef<${name}>('${name}', schemaDef),`
|
|
91
|
+
}).join('\n')
|
|
92
|
+
|
|
93
|
+
const allImports = [...entityNames, ...roleTypeImports]
|
|
94
|
+
|
|
95
|
+
return `import { entityDef, roleEntityDef } from '@contember/bindx'
|
|
96
|
+
import { schemaNamesToDef } from '@contember/bindx-react'
|
|
97
|
+
import type { ${allImports.join(', ')} } from './entities'
|
|
98
|
+
import { schemaNames } from './names'
|
|
99
|
+
|
|
100
|
+
const schemaDef = schemaNamesToDef(schemaNames)
|
|
101
|
+
|
|
102
|
+
export const schema = {
|
|
103
|
+
${entries}
|
|
104
|
+
} as const
|
|
105
|
+
`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolves role permissions, flattening inheritance.
|
|
110
|
+
* Returns a map: role → (entity → Set<fieldName>)
|
|
111
|
+
*/
|
|
112
|
+
private resolveRoles(acl: Acl.Schema): Record<string, Map<string, Set<string>>> {
|
|
113
|
+
const result: Record<string, Map<string, Set<string>>> = {}
|
|
114
|
+
|
|
115
|
+
for (const [roleName, rolePerms] of Object.entries(acl.roles)) {
|
|
116
|
+
// Skip implicit roles
|
|
117
|
+
if (rolePerms.implicit) continue
|
|
118
|
+
|
|
119
|
+
const entityFieldMap = new Map<string, Set<string>>()
|
|
120
|
+
|
|
121
|
+
// Collect inherited fields first
|
|
122
|
+
if (rolePerms.inherits) {
|
|
123
|
+
for (const parentRole of rolePerms.inherits) {
|
|
124
|
+
const parentFields = result[parentRole]
|
|
125
|
+
if (parentFields) {
|
|
126
|
+
for (const [entityName, fields] of parentFields) {
|
|
127
|
+
const existing = entityFieldMap.get(entityName) ?? new Set()
|
|
128
|
+
for (const field of fields) {
|
|
129
|
+
existing.add(field)
|
|
130
|
+
}
|
|
131
|
+
entityFieldMap.set(entityName, existing)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Add own permissions
|
|
138
|
+
for (const [entityName, entityPerms] of Object.entries(rolePerms.entities)) {
|
|
139
|
+
const readPerms = entityPerms.operations.read
|
|
140
|
+
if (!readPerms) continue
|
|
141
|
+
|
|
142
|
+
const fields = entityFieldMap.get(entityName) ?? new Set<string>()
|
|
143
|
+
for (const [fieldName, perm] of Object.entries(readPerms)) {
|
|
144
|
+
if (perm === true || (this.allowPredicateAccess && perm !== false)) {
|
|
145
|
+
fields.add(fieldName)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (fields.size > 0) {
|
|
149
|
+
entityFieldMap.set(entityName, fields)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
result[roleName] = entityFieldMap
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Generates per-role entity interfaces for a single role.
|
|
161
|
+
*/
|
|
162
|
+
private generateRoleEntityTypes(
|
|
163
|
+
model: Model.Schema,
|
|
164
|
+
roleName: string,
|
|
165
|
+
roleAccess: Map<string, Set<string>>,
|
|
166
|
+
): string {
|
|
167
|
+
let code = ''
|
|
168
|
+
|
|
169
|
+
for (const entity of Object.values(model.entities)) {
|
|
170
|
+
const accessibleFields = roleAccess.get(entity.name)
|
|
171
|
+
if (!accessibleFields || accessibleFields.size === 0) continue
|
|
172
|
+
|
|
173
|
+
code += this.generateRoleEntityType(model, entity, roleName, accessibleFields, roleAccess)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return code
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private generateRoleEntityType(
|
|
180
|
+
model: Model.Schema,
|
|
181
|
+
entity: Model.Entity,
|
|
182
|
+
roleName: string,
|
|
183
|
+
accessibleFields: Set<string>,
|
|
184
|
+
roleAccess: Map<string, Set<string>>,
|
|
185
|
+
): string {
|
|
186
|
+
const typeName = this.roleEntityName(roleName, entity.name)
|
|
187
|
+
let code = `export interface ${typeName} {\n`
|
|
188
|
+
|
|
189
|
+
acceptEveryFieldVisitor(model, entity, {
|
|
190
|
+
visitColumn: ctx => {
|
|
191
|
+
if (!accessibleFields.has(ctx.column.name)) return
|
|
192
|
+
code += `\t${ctx.column.name}: ${columnToTsType(ctx.column)}${ctx.column.nullable ? ' | null' : ''}\n`
|
|
193
|
+
},
|
|
194
|
+
visitHasOne: ctx => {
|
|
195
|
+
if (!accessibleFields.has(ctx.relation.name)) return
|
|
196
|
+
const targetFields = roleAccess.get(ctx.targetEntity.name)
|
|
197
|
+
const targetType = targetFields && targetFields.size > 0
|
|
198
|
+
? this.roleEntityName(roleName, ctx.targetEntity.name)
|
|
199
|
+
: ctx.targetEntity.name
|
|
200
|
+
code += `\t${ctx.relation.name}: ${targetType}\n`
|
|
201
|
+
},
|
|
202
|
+
visitHasMany: ctx => {
|
|
203
|
+
if (!accessibleFields.has(ctx.relation.name)) return
|
|
204
|
+
const targetFields = roleAccess.get(ctx.targetEntity.name)
|
|
205
|
+
const targetType = targetFields && targetFields.size > 0
|
|
206
|
+
? this.roleEntityName(roleName, ctx.targetEntity.name)
|
|
207
|
+
: ctx.targetEntity.name
|
|
208
|
+
code += `\t${ctx.relation.name}: ${targetType}[]\n`
|
|
209
|
+
},
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
code += '}\n\n'
|
|
213
|
+
return code
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private roleEntityName(roleName: string, entityName: string): string {
|
|
217
|
+
return `${entityName}$${roleName}`
|
|
218
|
+
}
|
|
219
|
+
}
|
package/src/generate.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Example script to generate bindx schema from Contember Model
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun run packages/bindx-generator/scripts/generate.ts ./path/to/model.ts ./path/to/output/dir
|
|
7
|
+
*
|
|
8
|
+
* This script demonstrates how to use the bindx generator to create
|
|
9
|
+
* TypeScript schema files from a Contember model.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { generate } from './index'
|
|
13
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
|
|
16
|
+
async function main(): Promise<void> {
|
|
17
|
+
console.log('Generating bindx schema...')
|
|
18
|
+
|
|
19
|
+
const schemaFile = process.argv[2]
|
|
20
|
+
const outputPath = process.argv[3]
|
|
21
|
+
|
|
22
|
+
if (!schemaFile || !outputPath) {
|
|
23
|
+
console.error('Usage: bun run generate.ts <schema-file> <output-dir>')
|
|
24
|
+
process.exit(1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const absoluteSchemaFile = join(process.cwd(), schemaFile)
|
|
28
|
+
const schema = (await import(absoluteSchemaFile)).default
|
|
29
|
+
|
|
30
|
+
// Generate schema files
|
|
31
|
+
const files = generate(schema.model)
|
|
32
|
+
|
|
33
|
+
// Output directory
|
|
34
|
+
const outputDir = join(process.cwd(), outputPath)
|
|
35
|
+
|
|
36
|
+
// Create output directory
|
|
37
|
+
await mkdir(outputDir, { recursive: true })
|
|
38
|
+
|
|
39
|
+
// Write files
|
|
40
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
41
|
+
const filePath = join(outputDir, filename)
|
|
42
|
+
await writeFile(filePath, String(content), 'utf-8')
|
|
43
|
+
console.log(`Generated: ${filePath}`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(`\nSchema generation complete!`)
|
|
47
|
+
console.log(`\nGenerated files in: ${outputDir}`)
|
|
48
|
+
console.log('\nTo use the generated schema:')
|
|
49
|
+
console.log(' import { useEntity, Entity } from "./generated"')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
main().catch(error => {
|
|
53
|
+
console.error('Error generating schema:', error)
|
|
54
|
+
process.exit(1)
|
|
55
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @contember/bindx-generator
|
|
3
|
+
*
|
|
4
|
+
* Schema generator for @contember/bindx.
|
|
5
|
+
* Generates TypeScript types and runtime schema definitions from
|
|
6
|
+
* Contember Model.Schema.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { BindxGenerator, generate } from './BindxGenerator'
|
|
10
|
+
export type { BindxGeneratorOptions, GeneratedFiles } from './BindxGenerator'
|
|
11
|
+
|
|
12
|
+
export { EntityTypeSchemaGenerator } from './EntityTypeSchemaGenerator'
|
|
13
|
+
export { EnumTypeSchemaGenerator } from './EnumTypeSchemaGenerator'
|
|
14
|
+
export { NameSchemaGenerator } from './NameSchemaGenerator'
|
|
15
|
+
export { RoleSchemaGenerator } from './RoleSchemaGenerator'
|
|
16
|
+
export type { RoleSchemaGeneratorOptions } from './RoleSchemaGenerator'
|
|
17
|
+
export type { BindxSchemaNames, BindxSchemaEntityNames } from './NameSchemaGenerator'
|
|
18
|
+
|
|
19
|
+
export { columnToTsType, getEnumTypeName, capitalizeFirstLetter } from './utils'
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for bindx schema generation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Model } from '@contember/schema'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert Contember column type to TypeScript type string
|
|
9
|
+
*/
|
|
10
|
+
export const columnToTsType = (column: Model.AnyColumn): string => {
|
|
11
|
+
const baseType = (() => {
|
|
12
|
+
switch (column.type) {
|
|
13
|
+
case Model.ColumnType.Enum:
|
|
14
|
+
return getEnumTypeName(column.columnType)
|
|
15
|
+
case Model.ColumnType.String:
|
|
16
|
+
return 'string'
|
|
17
|
+
case Model.ColumnType.Int:
|
|
18
|
+
return 'number'
|
|
19
|
+
case Model.ColumnType.Double:
|
|
20
|
+
return 'number'
|
|
21
|
+
case Model.ColumnType.Bool:
|
|
22
|
+
return 'boolean'
|
|
23
|
+
case Model.ColumnType.DateTime:
|
|
24
|
+
return 'string'
|
|
25
|
+
case Model.ColumnType.Time:
|
|
26
|
+
return 'string'
|
|
27
|
+
case Model.ColumnType.Date:
|
|
28
|
+
return 'string'
|
|
29
|
+
case Model.ColumnType.Json:
|
|
30
|
+
return 'JSONValue'
|
|
31
|
+
case Model.ColumnType.Uuid:
|
|
32
|
+
return 'string'
|
|
33
|
+
default:
|
|
34
|
+
((_: never) => {
|
|
35
|
+
throw new Error(`Unknown column type ${_}`)
|
|
36
|
+
})(column.type)
|
|
37
|
+
}
|
|
38
|
+
})()
|
|
39
|
+
return column.list ? `readonly ${baseType}[]` : baseType
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate TypeScript enum type name from Contember enum name
|
|
44
|
+
*/
|
|
45
|
+
export const getEnumTypeName = (enumName: string): string => {
|
|
46
|
+
return `${enumName}Enum`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Capitalize first letter of a string
|
|
51
|
+
*/
|
|
52
|
+
export const capitalizeFirstLetter = (value: string): string => {
|
|
53
|
+
return value.charAt(0).toUpperCase() + value.slice(1)
|
|
54
|
+
}
|