@api-client/core 0.18.26 → 0.18.28
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/build/src/modeling/ApiModel.d.ts +18 -9
- package/build/src/modeling/ApiModel.d.ts.map +1 -1
- package/build/src/modeling/ApiModel.js +141 -13
- package/build/src/modeling/ApiModel.js.map +1 -1
- package/build/src/modeling/Semantics.d.ts +7 -0
- package/build/src/modeling/Semantics.d.ts.map +1 -1
- package/build/src/modeling/Semantics.js +23 -0
- package/build/src/modeling/Semantics.js.map +1 -1
- package/build/src/modeling/helpers/endpointHelpers.d.ts +2 -0
- package/build/src/modeling/helpers/endpointHelpers.d.ts.map +1 -0
- package/build/src/modeling/helpers/endpointHelpers.js +6 -0
- package/build/src/modeling/helpers/endpointHelpers.js.map +1 -0
- package/build/src/modeling/helpers/keying.d.ts +9 -0
- package/build/src/modeling/helpers/keying.d.ts.map +1 -0
- package/build/src/modeling/helpers/keying.js +10 -0
- package/build/src/modeling/helpers/keying.js.map +1 -0
- package/build/src/modeling/templates/meta/blog-publishing-platform.json +1 -1
- package/build/src/modeling/templates/meta/financial-services-platform.json +1 -1
- package/build/src/modeling/templates/meta/index.d.ts +1 -1
- package/build/src/modeling/templates/meta/index.js +1 -1
- package/build/src/modeling/templates/meta/index.js.map +1 -1
- package/build/src/modeling/templates/verticals/business-services/financial-services-domain.d.ts.map +1 -1
- package/build/src/modeling/templates/verticals/business-services/financial-services-domain.js +1 -0
- package/build/src/modeling/templates/verticals/business-services/financial-services-domain.js.map +1 -1
- package/build/src/modeling/templates/verticals/technology-media/blog-domain.d.ts.map +1 -1
- package/build/src/modeling/templates/verticals/technology-media/blog-domain.js +12 -4
- package/build/src/modeling/templates/verticals/technology-media/blog-domain.js.map +1 -1
- package/build/src/modeling/types.d.ts +77 -8
- package/build/src/modeling/types.d.ts.map +1 -1
- package/build/src/modeling/types.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/data/models/example-generator-api.json +6 -6
- package/package.json +2 -1
- package/src/modeling/ApiModel.ts +159 -15
- package/src/modeling/Semantics.ts +23 -0
- package/src/modeling/helpers/endpointHelpers.ts +5 -0
- package/src/modeling/helpers/keying.ts +11 -0
- package/src/modeling/readme.md +153 -7
- package/src/modeling/templates/meta/blog-publishing-platform.json +1 -1
- package/src/modeling/templates/meta/financial-services-platform.json +1 -1
- package/src/modeling/templates/verticals/business-services/financial-services-domain.ts +1 -0
- package/src/modeling/templates/verticals/technology-media/blog-domain.ts +12 -4
- package/src/modeling/types.ts +84 -8
- package/tests/unit/modeling/api_model.spec.ts +25 -137
- package/tests/unit/modeling/api_model_expose_entity.spec.ts +190 -0
- package/tests/unit/modeling/api_model_remove_entity.spec.ts +82 -0
- package/tests/unit/modeling/helpers/endpointHelpers.spec.ts +10 -0
- package/tests/unit/modeling/username_semantic.spec.ts +81 -0
|
@@ -42068,10 +42068,10 @@
|
|
|
42068
42068
|
"@id": "#194"
|
|
42069
42069
|
},
|
|
42070
42070
|
{
|
|
42071
|
-
"@id": "#
|
|
42071
|
+
"@id": "#197"
|
|
42072
42072
|
},
|
|
42073
42073
|
{
|
|
42074
|
-
"@id": "#
|
|
42074
|
+
"@id": "#200"
|
|
42075
42075
|
},
|
|
42076
42076
|
{
|
|
42077
42077
|
"@id": "#203"
|
|
@@ -43478,7 +43478,7 @@
|
|
|
43478
43478
|
"doc:ExternalDomainElement",
|
|
43479
43479
|
"doc:DomainElement"
|
|
43480
43480
|
],
|
|
43481
|
-
"doc:raw": "
|
|
43481
|
+
"doc:raw": "code: '5'\ndescription: 'Limited company'\n",
|
|
43482
43482
|
"core:mediaType": "application/yaml",
|
|
43483
43483
|
"sourcemaps:sources": [
|
|
43484
43484
|
{
|
|
@@ -43499,7 +43499,7 @@
|
|
|
43499
43499
|
"doc:ExternalDomainElement",
|
|
43500
43500
|
"doc:DomainElement"
|
|
43501
43501
|
],
|
|
43502
|
-
"doc:raw": "
|
|
43502
|
+
"doc:raw": "class: '3'\ndescription: '150 - 300'\nnumberOfFte: 5500\nnumberOfEmployees: 5232\n",
|
|
43503
43503
|
"core:mediaType": "application/yaml",
|
|
43504
43504
|
"sourcemaps:sources": [
|
|
43505
43505
|
{
|
|
@@ -44766,12 +44766,12 @@
|
|
|
44766
44766
|
{
|
|
44767
44767
|
"@id": "#199/source-map/lexical/element_0",
|
|
44768
44768
|
"sourcemaps:element": "amf://id#199",
|
|
44769
|
-
"sourcemaps:value": "[(1,0)-(
|
|
44769
|
+
"sourcemaps:value": "[(1,0)-(3,0)]"
|
|
44770
44770
|
},
|
|
44771
44771
|
{
|
|
44772
44772
|
"@id": "#202/source-map/lexical/element_0",
|
|
44773
44773
|
"sourcemaps:element": "amf://id#202",
|
|
44774
|
-
"sourcemaps:value": "[(1,0)-(
|
|
44774
|
+
"sourcemaps:value": "[(1,0)-(5,0)]"
|
|
44775
44775
|
},
|
|
44776
44776
|
{
|
|
44777
44777
|
"@id": "#205/source-map/lexical/element_0",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@api-client/core",
|
|
3
3
|
"description": "The API Client's core client library. Works in NodeJS and in a ES enabled browser.",
|
|
4
|
-
"version": "0.18.
|
|
4
|
+
"version": "0.18.28",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./browser.js": {
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"@api-client/graph": "^0.3.5",
|
|
88
88
|
"@api-client/json": "^0.2.0",
|
|
89
89
|
"@esm-bundle/chai": "^4.3.4-fix.0",
|
|
90
|
+
"@jarrodek/pluralize": "^1.0.2",
|
|
90
91
|
"@metrichor/jmespath": "^0.3.1",
|
|
91
92
|
"@pawel-up/csv": "^0.2.0",
|
|
92
93
|
"@pawel-up/data-mock": "^0.4.0",
|
package/src/modeling/ApiModel.ts
CHANGED
|
@@ -11,10 +11,13 @@ import type {
|
|
|
11
11
|
RolesBasedAccessControl,
|
|
12
12
|
SessionConfiguration,
|
|
13
13
|
UsernamePasswordConfiguration,
|
|
14
|
+
ExposeOptions,
|
|
14
15
|
} from './types.js'
|
|
15
16
|
import { DataDomain } from './DataDomain.js'
|
|
16
17
|
import { DependentModel, type DependentModelSchema, type DomainDependency } from './DependentModel.js'
|
|
17
18
|
import { observed, toRaw } from '../decorators/observed.js'
|
|
19
|
+
import pluralize from '@jarrodek/pluralize'
|
|
20
|
+
import { createDomainKey } from './helpers/keying.js'
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Contact information for the exposed API.
|
|
@@ -54,12 +57,12 @@ export interface ApiModelSchema extends DependentModelSchema {
|
|
|
54
57
|
*/
|
|
55
58
|
kind: typeof ApiModelKind
|
|
56
59
|
/**
|
|
57
|
-
* The unique key of the
|
|
60
|
+
* The unique key of the API model schema.
|
|
58
61
|
* This is a stable identifier that does not change across versions.
|
|
59
62
|
*/
|
|
60
63
|
key: string
|
|
61
64
|
/**
|
|
62
|
-
* Contains the name, display name, description, and the version of the
|
|
65
|
+
* Contains the name, display name, description, and the version of the API model schema.
|
|
63
66
|
*/
|
|
64
67
|
info: IThing
|
|
65
68
|
|
|
@@ -376,41 +379,182 @@ export class ApiModel extends DependentModel {
|
|
|
376
379
|
/**
|
|
377
380
|
* Exposes a new entity in the API model.
|
|
378
381
|
* If the entity already exists, it returns the existing one.
|
|
379
|
-
* @param
|
|
382
|
+
* @param entity The entity key and domain to expose.
|
|
380
383
|
* @returns The exposed entity.
|
|
381
384
|
*/
|
|
382
|
-
exposeEntity(
|
|
383
|
-
const
|
|
385
|
+
exposeEntity(entity: AssociationTarget, options?: ExposeOptions): ExposedEntity {
|
|
386
|
+
const domain = this.domain
|
|
387
|
+
if (!domain) {
|
|
388
|
+
throw new Error(`No domain attached to API model`)
|
|
389
|
+
}
|
|
390
|
+
// checks whether the entity is already exposed as a root exposure.
|
|
391
|
+
const existing = this.exposes.find(
|
|
392
|
+
(e) => e.isRoot && e.entity.key === entity.key && e.entity.domain === entity.domain
|
|
393
|
+
)
|
|
384
394
|
if (existing) {
|
|
395
|
+
// quietly return the existing exposure
|
|
396
|
+
// TBD: should we throw an error here?
|
|
385
397
|
return existing
|
|
386
398
|
}
|
|
399
|
+
const domainEntity = domain.findEntity(entity.key, entity.domain)
|
|
400
|
+
if (!domainEntity) {
|
|
401
|
+
throw new Error(`Entity not found in domain: ${entity.key}`)
|
|
402
|
+
}
|
|
403
|
+
const name = domainEntity.info.name || ''
|
|
387
404
|
const newEntity: ExposedEntity = {
|
|
388
|
-
key:
|
|
405
|
+
key: nanoid(),
|
|
406
|
+
entity: { ...entity },
|
|
389
407
|
actions: [],
|
|
408
|
+
isRoot: true,
|
|
409
|
+
path: pluralize(name.toLocaleLowerCase()),
|
|
410
|
+
}
|
|
411
|
+
if (options) {
|
|
412
|
+
newEntity.exposeOptions = { ...options }
|
|
390
413
|
}
|
|
391
414
|
this.exposes.push(newEntity)
|
|
415
|
+
|
|
416
|
+
// Follow associations if requested
|
|
417
|
+
if (options?.followAssociations) {
|
|
418
|
+
if (options?.maxDepth === undefined || options.maxDepth > 0) {
|
|
419
|
+
this.followEntityAssociations(newEntity, options)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
392
422
|
this.notifyChange()
|
|
393
423
|
return newEntity
|
|
394
424
|
}
|
|
395
425
|
|
|
396
426
|
/**
|
|
397
|
-
*
|
|
398
|
-
*
|
|
427
|
+
* Follows associations for a newly exposed entity if configured to do so.
|
|
428
|
+
* This creates nested exposures based on the entity's associations.
|
|
429
|
+
*
|
|
430
|
+
* @param parentExposure The root exposure to follow associations from
|
|
431
|
+
* @param options The expose options containing follow configuration
|
|
399
432
|
*/
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
if (
|
|
403
|
-
|
|
404
|
-
|
|
433
|
+
private followEntityAssociations(parentExposure: ExposedEntity, options: ExposeOptions): void {
|
|
434
|
+
const domain = this.domain
|
|
435
|
+
if (!domain) {
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
const maxDepth = options.maxDepth ?? 6
|
|
439
|
+
const visited = new Set<string>()
|
|
440
|
+
// Add parent entity's key to the visited set so we won't skip it when traversing
|
|
441
|
+
// associations.
|
|
442
|
+
visited.add(createDomainKey(parentExposure.entity))
|
|
443
|
+
|
|
444
|
+
const follow = (currentEntity: AssociationTarget, parentKey: string, depth: number) => {
|
|
445
|
+
// Find the domain entity
|
|
446
|
+
const domainEntity = domain.findEntity(currentEntity.key, currentEntity.domain)
|
|
447
|
+
if (!domainEntity) return
|
|
448
|
+
|
|
449
|
+
// Iterate through associations
|
|
450
|
+
for (const association of domainEntity.listAssociations()) {
|
|
451
|
+
for (const target of association.targets) {
|
|
452
|
+
// Skip self-referencing associations
|
|
453
|
+
if (target.key === currentEntity.key && target.domain === currentEntity.domain) {
|
|
454
|
+
continue
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Create unique identifier for circular detection
|
|
458
|
+
const visitKey = createDomainKey(target)
|
|
459
|
+
if (visited.has(visitKey)) {
|
|
460
|
+
continue // Skip circular references
|
|
461
|
+
}
|
|
462
|
+
visited.add(visitKey)
|
|
463
|
+
|
|
464
|
+
// Check if this nested exposure already exists
|
|
465
|
+
const existingNested = this.exposes.find(
|
|
466
|
+
(e) =>
|
|
467
|
+
!e.isRoot &&
|
|
468
|
+
e.entity.key === target.key &&
|
|
469
|
+
e.entity.domain === target.domain &&
|
|
470
|
+
e.parent?.key === parentKey
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
if (existingNested) {
|
|
474
|
+
continue // Already exposed under this parent
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Find the target domain entity for path generation
|
|
478
|
+
const targetDomainEntity = domain.findEntity(target.key, target.domain)
|
|
479
|
+
if (!targetDomainEntity) continue
|
|
480
|
+
|
|
481
|
+
const name = association.info.name || ''
|
|
482
|
+
// Create nested exposure
|
|
483
|
+
const nestedExposure: ExposedEntity = {
|
|
484
|
+
key: nanoid(),
|
|
485
|
+
entity: { ...target },
|
|
486
|
+
actions: [],
|
|
487
|
+
isRoot: false,
|
|
488
|
+
path: pluralize(name.toLocaleLowerCase()),
|
|
489
|
+
parent: {
|
|
490
|
+
key: parentKey,
|
|
491
|
+
association: {
|
|
492
|
+
key: association.key,
|
|
493
|
+
domain: currentEntity.domain,
|
|
494
|
+
},
|
|
495
|
+
depth: depth + 1,
|
|
496
|
+
},
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
this.exposes.push(nestedExposure)
|
|
500
|
+
if (depth + 1 >= maxDepth) {
|
|
501
|
+
nestedExposure.truncated = true
|
|
502
|
+
} else {
|
|
503
|
+
// Recursively follow associations
|
|
504
|
+
follow(target, nestedExposure.key, depth + 1)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Start following from the root exposure
|
|
511
|
+
follow(parentExposure.entity, parentExposure.key, 0)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Removes an exposed entity from the API model.
|
|
516
|
+
* @param entity The entity to remove.
|
|
517
|
+
*/
|
|
518
|
+
removeEntity(entity: AssociationTarget): void {
|
|
519
|
+
const current = this.exposes.find((e) => e.entity.key === entity.key && e.entity.domain === entity.domain)
|
|
520
|
+
if (!current) {
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
this.removeWithChildren(current.key)
|
|
524
|
+
this.notifyChange()
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private removeWithChildren(key: string): void {
|
|
528
|
+
const index = this.exposes.findIndex((e) => e.key === key)
|
|
529
|
+
if (index < 0) {
|
|
530
|
+
return
|
|
405
531
|
}
|
|
532
|
+
// Remove the parent itself
|
|
533
|
+
this.exposes.splice(index, 1)
|
|
534
|
+
// Remove all children recursively
|
|
535
|
+
const removeChildren = (parentKey: string) => {
|
|
536
|
+
// Find all exposures whose parent.key matches parentKey
|
|
537
|
+
const children = this.exposes.filter((e) => e.parent?.key === parentKey)
|
|
538
|
+
for (const child of children) {
|
|
539
|
+
removeChildren(child.key)
|
|
540
|
+
const childIndex = this.exposes.findIndex((e) => e.key === child.key)
|
|
541
|
+
if (childIndex >= 0) {
|
|
542
|
+
this.exposes.splice(childIndex, 1)
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// Then also remove children
|
|
547
|
+
removeChildren(key)
|
|
548
|
+
this.notifyChange()
|
|
406
549
|
}
|
|
550
|
+
|
|
407
551
|
/**
|
|
408
552
|
* Returns the exposed entity by its key.
|
|
409
553
|
* @param entityKey The key of the entity to find.
|
|
410
554
|
* @returns The exposed entity or undefined if not found.
|
|
411
555
|
*/
|
|
412
|
-
getExposedEntity(
|
|
413
|
-
return this.exposes.find((e) => e.key ===
|
|
556
|
+
getExposedEntity(entity: AssociationTarget): ExposedEntity | undefined {
|
|
557
|
+
return this.exposes.find((e) => e.entity.key === entity.key && e.entity.domain === entity.domain)
|
|
414
558
|
}
|
|
415
559
|
|
|
416
560
|
/**
|
|
@@ -26,6 +26,13 @@ export enum SemanticType {
|
|
|
26
26
|
* ensuring it is encrypted and not exposed in API responses.
|
|
27
27
|
*/
|
|
28
28
|
Password = 'Semantic#Password',
|
|
29
|
+
/**
|
|
30
|
+
* Annotates the field as the username for user authentication.
|
|
31
|
+
* This identifies which field should be used for login purposes.
|
|
32
|
+
* Can be applied to dedicated username fields or email fields that serve as usernames.
|
|
33
|
+
* The runtime uses this for authentication, password reset, and user lookup operations.
|
|
34
|
+
*/
|
|
35
|
+
Username = 'Semantic#Username',
|
|
29
36
|
/**
|
|
30
37
|
* Designates a Data Property as the `createdAt` timestamp of an entity.
|
|
31
38
|
* This is used to track when the entity was first created.
|
|
@@ -651,6 +658,22 @@ export const DataSemantics: Record<SemanticType, DataSemantic> = {
|
|
|
651
658
|
],
|
|
652
659
|
},
|
|
653
660
|
},
|
|
661
|
+
[SemanticType.Username]: {
|
|
662
|
+
id: SemanticType.Username,
|
|
663
|
+
displayName: 'Username',
|
|
664
|
+
scope: SemanticScope.Property,
|
|
665
|
+
description: 'User authentication identifier',
|
|
666
|
+
category: SemanticCategory.Identity,
|
|
667
|
+
applicableDataTypes: ['string'],
|
|
668
|
+
hasConfig: false,
|
|
669
|
+
runtime: {
|
|
670
|
+
timing: SemanticTiming.Before,
|
|
671
|
+
operations: [SemanticOperation.Create, SemanticOperation.Update, SemanticOperation.Read],
|
|
672
|
+
priority: 15, // High priority for authentication
|
|
673
|
+
canDisable: false, // Security semantics cannot be disabled
|
|
674
|
+
timeoutMs: 100, // Fast operation
|
|
675
|
+
},
|
|
676
|
+
},
|
|
654
677
|
[SemanticType.UserRole]: {
|
|
655
678
|
id: SemanticType.UserRole,
|
|
656
679
|
displayName: 'User Role Field',
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AssociationTarget } from '../types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a consistent key for a given association target within its domain.
|
|
5
|
+
*
|
|
6
|
+
* @param target The association target to create a key for.
|
|
7
|
+
* @returns A unique key for the association target.
|
|
8
|
+
*/
|
|
9
|
+
export function createDomainKey(target: AssociationTarget): string {
|
|
10
|
+
return target.domain ? `${target.domain}:${target.key}` : target.key
|
|
11
|
+
}
|
package/src/modeling/readme.md
CHANGED
|
@@ -122,13 +122,159 @@ Let's imagine a simple e-commerce domain:
|
|
|
122
122
|
- **Entity-Parent**: A `DomainEntity` can have `DomainEntity` instances as parents.
|
|
123
123
|
- **Namespace-Foreign**: A `DomainNamespace` can have references to `DomainNamespace` instances.
|
|
124
124
|
|
|
125
|
+
## API Modeling
|
|
126
|
+
|
|
127
|
+
Beyond data modeling, the system also provides **API Modeling** capabilities that allow you to define how your data domain is exposed as a secure, production-ready API.
|
|
128
|
+
|
|
129
|
+
### ApiModel
|
|
130
|
+
|
|
131
|
+
The `ApiModel` class extends the data modeling system to define how Data Entities from a `DataDomain` are exposed via HTTP APIs. It provides configuration for:
|
|
132
|
+
|
|
133
|
+
**Core API Configuration:**
|
|
134
|
+
|
|
135
|
+
- **Exposed Entities**: Select which Data Entities from your Data Domain should be accessible via the API
|
|
136
|
+
- **API Actions**: Define what operations (List, Read, Create, Update, Delete, Search) are available for each entity
|
|
137
|
+
- **User Entity**: Designate which Data Entity represents a "User" for authentication purposes
|
|
138
|
+
|
|
139
|
+
**Security & Authentication:**
|
|
140
|
+
|
|
141
|
+
- **Authentication**: Configure how users prove their identity (e.g., username/password)
|
|
142
|
+
- **Authorization**: Define what authenticated users are allowed to do (e.g., Role-Based Access Control)
|
|
143
|
+
- **Session Management**: Configure transport and payload for user sessions (JWT, cookies)
|
|
144
|
+
- **Access Rules**: Define fine-grained access control policies for entities and actions
|
|
145
|
+
|
|
146
|
+
**API Metadata:**
|
|
147
|
+
|
|
148
|
+
- **Contact Information**: API maintainer details
|
|
149
|
+
- **License Information**: Legal information about API usage
|
|
150
|
+
- **Terms of Service**: Link to API usage terms
|
|
151
|
+
- **Rate Limiting**: Protect the API from overuse and abuse
|
|
152
|
+
|
|
153
|
+
### How API Modeling Works with Data Modeling
|
|
154
|
+
|
|
155
|
+
The API modeling system builds on top of the data modeling foundation:
|
|
156
|
+
|
|
157
|
+
1. **Start with a Data Domain**: Create your `DataDomain` with entities, properties, and associations
|
|
158
|
+
2. **Create an API Model**: Instantiate an `ApiModel` and attach your `DataDomain`
|
|
159
|
+
3. **Expose Entities**: Select which entities should be available via the API using `exposeEntity()`
|
|
160
|
+
4. **Configure Security**: Set up authentication, authorization, and session management
|
|
161
|
+
5. **Define Actions**: Configure what operations are available for each exposed entity
|
|
162
|
+
6. **Set API Metadata**: Add contact info, license, terms of service
|
|
163
|
+
|
|
164
|
+
### Example: E-Commerce API
|
|
165
|
+
|
|
166
|
+
Building on the e-commerce data domain example:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// 1. Create the data domain (as shown in previous example)
|
|
170
|
+
const domain = new DataDomain({ key: 'ecommerce', info: { name: 'E-Commerce Platform' } })
|
|
171
|
+
const userModel = domain.addModel({ info: { name: 'User Management' } })
|
|
172
|
+
const userEntity = userModel.addEntity({
|
|
173
|
+
info: { name: 'user' },
|
|
174
|
+
semantics: [{ type: SemanticType.User }] // Mark as User entity
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// 2. Create an API model
|
|
178
|
+
const apiModel = new ApiModel({
|
|
179
|
+
info: { name: 'E-Commerce API', version: '1.0.0' },
|
|
180
|
+
termsOfService: 'https://example.com/terms',
|
|
181
|
+
contact: { name: 'API Team', email: 'api@example.com' },
|
|
182
|
+
license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' }
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// 3. Attach the data domain
|
|
186
|
+
apiModel.attachDataDomain(domain)
|
|
187
|
+
|
|
188
|
+
// 4. Configure security
|
|
189
|
+
apiModel.user = { key: userEntity.key }
|
|
190
|
+
apiModel.authentication = { strategy: 'UsernamePassword', passwordKey: 'password' }
|
|
191
|
+
apiModel.authorization = { strategy: 'RBAC', roleKey: 'role' }
|
|
192
|
+
apiModel.session = {
|
|
193
|
+
secret: 'your-secure-secret',
|
|
194
|
+
properties: ['email', 'role'],
|
|
195
|
+
cookie: { enabled: true, kind: 'cookie', lifetime: '7d' }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 5. Expose entities and configure actions
|
|
199
|
+
const exposedUser = apiModel.exposeEntity(userEntity.key)
|
|
200
|
+
exposedUser.actions = [
|
|
201
|
+
{ type: 'Read', enabled: true },
|
|
202
|
+
{ type: 'Update', enabled: true },
|
|
203
|
+
{ type: 'Create', enabled: true }
|
|
204
|
+
]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Key API Modeling Concepts
|
|
208
|
+
|
|
209
|
+
#### ExposedEntity
|
|
210
|
+
|
|
211
|
+
Represents a Data Entity that is exposed via the API. Contains:
|
|
212
|
+
|
|
213
|
+
- **Key**: Reference to the Data Entity
|
|
214
|
+
- **Actions**: List of available API operations (CRUD, Search)
|
|
215
|
+
- **Access Rules**: Entity-specific access control
|
|
216
|
+
- **Rate Limiting**: Entity-specific rate limiting rules
|
|
217
|
+
|
|
218
|
+
#### API Actions
|
|
219
|
+
|
|
220
|
+
Standard RESTful operations that can be enabled for each entity:
|
|
221
|
+
|
|
222
|
+
- **List**: Retrieve collections of entities with filtering and pagination
|
|
223
|
+
- **Read**: Retrieve a single entity by ID
|
|
224
|
+
- **Create**: Create new entities
|
|
225
|
+
- **Update**: Modify existing entities
|
|
226
|
+
- **Delete**: Remove entities
|
|
227
|
+
- **Search**: Full-text or advanced search across entities
|
|
228
|
+
|
|
229
|
+
#### Authentication Configuration
|
|
230
|
+
|
|
231
|
+
Defines how users prove their identity:
|
|
232
|
+
|
|
233
|
+
- **UsernamePassword**: Traditional email/password authentication
|
|
234
|
+
- Extensible for future strategies (SSO, OAuth, etc.)
|
|
235
|
+
|
|
236
|
+
#### Authorization Configuration
|
|
237
|
+
|
|
238
|
+
Defines what authenticated users can do:
|
|
239
|
+
|
|
240
|
+
- **RBAC (Role-Based Access Control)**: Users have roles, permissions granted to roles
|
|
241
|
+
- Extensible for future strategies (PBAC - Permission-Based Access Control)
|
|
242
|
+
|
|
243
|
+
#### Session Configuration
|
|
244
|
+
|
|
245
|
+
Manages user session data:
|
|
246
|
+
|
|
247
|
+
- **Properties**: Which User entity properties to include in session
|
|
248
|
+
- **Transport**: How session data is transmitted (JWT tokens, cookies)
|
|
249
|
+
- **Security**: Encryption, lifetime, and security settings
|
|
250
|
+
|
|
251
|
+
### Business-First API Design
|
|
252
|
+
|
|
253
|
+
The API modeling system follows the same business-first philosophy as data modeling:
|
|
254
|
+
|
|
255
|
+
- **Semantic Awareness**: Uses semantic types to automatically configure security (e.g., Password properties are write-only)
|
|
256
|
+
- **Standard Compliance**: Generates OpenAPI-compliant specifications
|
|
257
|
+
- **Security by Default**: Requires explicit configuration for authentication, authorization, and sessions
|
|
258
|
+
- **User-Friendly**: Clear error messages and validation feedback
|
|
259
|
+
- **Production Ready**: Built-in support for rate limiting, access control, and monitoring
|
|
260
|
+
|
|
125
261
|
## Summary
|
|
126
262
|
|
|
127
|
-
This
|
|
263
|
+
This comprehensive modeling system provides both **Data Modeling** and **API Modeling** capabilities:
|
|
264
|
+
|
|
265
|
+
**Data Modeling** allows you to:
|
|
266
|
+
|
|
267
|
+
- **Organize**: Group related data into namespaces and models
|
|
268
|
+
- **Structure**: Define entities with properties and relationships
|
|
269
|
+
- **Reuse**: Inherit from other entities and reference foreign namespaces
|
|
270
|
+
- **Translate**: Define bindings to map the model to different formats
|
|
271
|
+
- **Validate**: Validate the data model definition
|
|
272
|
+
- **Generate**: Generate AMF shapes and examples
|
|
273
|
+
|
|
274
|
+
**API Modeling** extends this to:
|
|
128
275
|
|
|
129
|
-
- **
|
|
130
|
-
- **
|
|
131
|
-
- **
|
|
132
|
-
- **
|
|
133
|
-
- **
|
|
134
|
-
- **Generate**: Generate AMF shapes and examples.
|
|
276
|
+
- **Expose**: Selectively expose Data Entities as API resources
|
|
277
|
+
- **Secure**: Configure authentication, authorization, and session management
|
|
278
|
+
- **Control**: Define fine-grained access rules and rate limiting
|
|
279
|
+
- **Document**: Generate complete API documentation with business context
|
|
280
|
+
- **Deploy**: Create production-ready APIs with proper security and monitoring
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"blog-publishing-platform","name":"Blog Publishing Platform","description":"A comprehensive content management and publishing platform for blogs, magazines, and digital publications. Includes content management, user roles, publishing workflow, and social features.","createdAt":"2025-07-27T21:14:57.328Z","updatedAt":"2025-07-27T21:14:57.328Z","version":"1.0.0","author":"API Now! Core Team","tags":["blog","cms","publishing","content","media","editorial"],"structure":{"domain":{"name":"Blog Publishing Platform","description":"A comprehensive content management and publishing platform for blogs, magazines, and digital publications","totalEntities":8,"totalProperties":69,"totalAssociations":14},"namespaces":[{"name":"ContentManagement","displayName":"Content Management","description":"Core content creation, editing, and organization features","modelCount":2,"entityCount":4,"models":[{"name":"Publications","displayName":"Publications Management","description":"Individual blogs, magazines, or publications within the platform","entityCount":1,"entities":[{"name":"publication","displayName":"Publication","description":"Individual blog or publication site","propertyCount":8,"associationCount":0,"properties":[{"name":"id","displayName":"Publication ID","description":"Unique identifier for the publication","type":"string","primary":true,"readOnly":true,"semantics":[]},{"name":"name","displayName":"Publication Name","description":"Display name of the publication","type":"string","semantics":["Semantic#Title"]},{"name":"slug","displayName":"URL Slug","description":"URL-friendly identifier for the publication","type":"string","required":true,"unique":true,"semantics":["Semantic#PublicUniqueName"]},{"name":"description","displayName":"Description","description":"Publication description and tagline","type":"string","semantics":["Semantic#Description"]},{"name":"domain","displayName":"Custom Domain","description":"Custom domain name for the publication","type":"string","semantics":[]},{"name":"logo_url","displayName":"Logo URL","description":"URL to publication logo image","type":"string","semantics":["Semantic#ImageURL"]},{"name":"status","displayName":"Publication Status","description":"Current status of the publication","type":"string","required":true,"semantics":["Semantic#Status"],"enumValues":["active","suspended","archived"],"defaultValue":"active"},{"name":"created_at","displayName":"Created At","description":"When the publication was created","type":"datetime","readOnly":true,"semantics":["Semantic#CreatedTimestamp"]}],"associations":[],"semantics":[]}]},{"name":"Content","displayName":"Content Management","description":"Posts, pages, and other content types","entityCount":3,"entities":[{"name":"category","displayName":"Content Category","description":"Content categorization for organization and navigation","propertyCount":5,"associationCount":2,"properties":[{"name":"id","displayName":"Category ID","description":"Unique identifier for the category","type":"string","primary":true,"readOnly":true,"semantics":[]},{"name":"name","displayName":"Category Name","description":"Display name of the category","type":"string","semantics":["Semantic#Title"]},{"name":"slug","displayName":"URL Slug","description":"URL-friendly identifier for the category","type":"string","required":true,"unique":true,"semantics":["Semantic#PublicUniqueName"]},{"name":"description","displayName":"Description","description":"Category description","type":"string","semantics":["Semantic#Description"]},{"name":"color","displayName":"Color","description":"Theme color for the category","type":"string","semantics":[]}],"associations":[{"name":"publication","displayName":"Publication","description":"Publication this category belongs to","required":true,"multiple":false,"targetEntities":["publication"],"semantics":[],"cardinality":"One-to-One"},{"name":"parentCategory","displayName":"Parent Category","description":"Parent category for hierarchical organization","required":false,"multiple":false,"targetEntities":["category"],"semantics":[],"cardinality":"One-to-One"}],"semantics":[]},{"name":"Tag","displayName":"Content Tag","description":"Tags for flexible content labeling and discovery","propertyCount":3,"associationCount":1,"properties":[{"name":"id","displayName":"Tag ID","description":"Unique identifier for the tag","type":"string","primary":true,"readOnly":true,"semantics":[]},{"name":"name","displayName":"Tag Name","description":"Display name of the tag","type":"string","semantics":["Semantic#Title"]},{"name":"slug","displayName":"URL Slug","description":"URL-friendly identifier for the tag","type":"string","required":true,"unique":true,"semantics":["Semantic#PublicUniqueName"]}],"associations":[{"name":"publication","displayName":"Publication","description":"Publication this tag belongs to","required":true,"multiple":false,"targetEntities":["publication"],"semantics":[],"cardinality":"One-to-One"}],"semantics":[]},{"name":"Post","displayName":"Blog Post","description":"Individual blog posts and articles","propertyCount":16,"associationCount":4,"properties":[{"name":"id","displayName":"Post ID","description":"Unique identifier for the post","type":"string","primary":true,"readOnly":true,"semantics":[]},{"name":"title","displayName":"Post Title","description":"Title of the blog post","type":"string","required":true,"semantics":["Semantic#Title"]},{"name":"slug","displayName":"URL Slug","description":"URL-friendly identifier for the post","type":"string","required":true,"unique":true,"semantics":["Semantic#PublicUniqueName"]},{"name":"excerpt","displayName":"Excerpt","description":"Brief summary or excerpt of the post","type":"string","semantics":["Semantic#Summary"]},{"name":"content","displayName":"Content","description":"Full content of the post in HTML or Markdown","type":"string","required":true,"semantics":["Semantic#HTML"]},{"name":"content_format","displayName":"Content Format","description":"Format of the content (HTML, Markdown, etc.)","type":"string","required":true,"semantics":[],"enumValues":["html","markdown","richtext"],"defaultValue":"markdown"},{"name":"featured_image_url","displayName":"Featured Image URL","description":"URL to featured image","type":"string","semantics":["Semantic#ImageURL"]},{"name":"status","displayName":"Post Status","description":"Current publishing status of the post","type":"string","required":true,"semantics":["Semantic#Status"],"enumValues":["draft","pending_review","scheduled","published","archived"],"defaultValue":"draft"},{"name":"published_at","displayName":"Published At","description":"When the post was published","type":"datetime","readOnly":true,"semantics":["Semantic#CreatedTimestamp"]},{"name":"scheduled_at","displayName":"Scheduled At","description":"When the post is scheduled to be published","type":"datetime","semantics":[]},{"name":"view_count","displayName":"View Count","description":"Number of times the post has been viewed","type":"number","required":true,"semantics":[]},{"name":"reading_time","displayName":"Reading Time","description":"Estimated reading time in minutes","type":"number","readOnly":true,"semantics":["Semantic#Calculated"]},{"name":"word_count","displayName":"Word Count","description":"Number of words in the post content","type":"number","readOnly":true,"semantics":["Semantic#Calculated"]},{"name":"meta_title","displayName":"Meta Title","description":"SEO meta title","type":"string","semantics":[]},{"name":"meta_description","displayName":"Meta Description","description":"SEO meta description","type":"string","semantics":[]},{"name":"updated_at","displayName":"Updated At","description":"When the post was last updated","type":"datetime","readOnly":true,"semantics":["Semantic#UpdatedTimestamp"]}],"associations":[{"name":"publication","displayName":"Publication","description":"Publication this post belongs to","required":true,"multiple":false,"targetEntities":["publication"],"semantics":[],"cardinality":"One-to-One"},{"name":"categories","displayName":"Post Categories","description":"Categories this post belongs to","required":false,"multiple":true,"targetEntities":["category"],"semantics":["Semantic#Categories"],"cardinality":"One-to-Many"},{"name":"tags","displayName":"Post Tags","description":"Tags associated with this post","required":false,"multiple":true,"targetEntities":["Tag"],"semantics":["Semantic#Tags"],"cardinality":"One-to-Many"},{"name":"author","displayName":"Post Author","description":"Author who wrote this post","required":true,"multiple":false,"targetEntities":["user"],"semantics":["Semantic#ResourceOwnerIdentifier"],"cardinality":"One-to-One"}],"semantics":[]}]}]},{"name":"UserManagement","displayName":"User Management","description":"Authors, editors, subscribers, and user roles management","modelCount":1,"entityCount":1,"models":[{"name":"Users","displayName":"User Management","description":"User accounts and authentication","entityCount":1,"entities":[{"name":"user","displayName":"User Account","description":"User account for authors, editors, and subscribers","propertyCount":13,"associationCount":0,"properties":[{"name":"id","displayName":"User ID","description":"Unique identifier for the user","type":"string","primary":true,"readOnly":true,"semantics":[]},{"name":"email","displayName":"Email Address","description":"User email address for login and communication","type":"string","required":true,"semantics":["Semantic#Email"]},{"name":"password","displayName":"Password","description":"Encrypted password for authentication","type":"string","required":true,"semantics":["Semantic#Password"]},{"name":"username","displayName":"Username","description":"Unique username for the user","type":"string","required":true,"unique":true,"semantics":["Semantic#PublicUniqueName"]},{"name":"display_name","displayName":"Display Name","description":"Public display name for the user","type":"string","required":true,"semantics":[]},{"name":"first_name","displayName":"First Name","description":"User first name","type":"string","semantics":[]},{"name":"last_name","displayName":"Last Name","description":"User last name","type":"string","semantics":[]},{"name":"bio","displayName":"Biography","description":"User biography and description","type":"string","semantics":["Semantic#Description"]},{"name":"avatar_url","displayName":"Avatar URL","description":"URL to user profile picture","type":"string","semantics":["Semantic#ImageURL"]},{"name":"website","displayName":"Website","description":"User personal website URL","type":"string","semantics":["Semantic#URL"]},{"name":"role","displayName":"User Role","description":"User role for permission management","type":"string","required":true,"semantics":["Semantic#Status"],"enumValues":["subscriber","author","editor","admin","super_admin"],"defaultValue":"subscriber"},{"name":"emailVerified","displayName":"Email Verified","description":"Whether the user has verified their email","type":"boolean","required":true,"semantics":[],"defaultValue":"false"},{"name":"created_at","displayName":"Created At","description":"When the user account was created","type":"datetime","readOnly":true,"semantics":["Semantic#CreatedTimestamp"]}],"associations":[],"semantics":["Semantic#User"]}]}]},{"name":"SocialFeatures","displayName":"Social Features","description":"Comments, likes, shares, and social interactions","modelCount":1,"entityCount":1,"models":[{"name":"Comments","displayName":"Comment System","description":"Post comments and replies","entityCount":1,"entities":[{"name":"comment","displayName":"Comment","description":"User comments on posts","propertyCount":7,"associationCount":3,"properties":[{"name":"id","displayName":"Comment ID","description":"Unique identifier for the comment","type":"string","primary":true,"readOnly":true,"semantics":[]},{"name":"content","displayName":"Comment Content","description":"Content of the comment","type":"string","required":true,"semantics":[]},{"name":"author_name","displayName":"Author Name","description":"Name of the comment author (for guest comments)","type":"string","semantics":[]},{"name":"author_email","displayName":"Author Email","description":"Email of the comment author (for guest comments)","type":"string","required":true,"semantics":["Semantic#Email"]},{"name":"status","displayName":"Comment Status","description":"Moderation status of the comment","type":"string","required":true,"semantics":["Semantic#Status"],"enumValues":["pending","approved","rejected","spam"],"defaultValue":"pending"},{"name":"user_agent","displayName":"User Agent","description":"Browser user agent string","type":"string","semantics":[]},{"name":"created_at","displayName":"Created At","description":"When the comment was created","type":"datetime","readOnly":true,"semantics":["Semantic#CreatedTimestamp"]}],"associations":[{"name":"post","displayName":"Post","description":"Post this comment belongs to","required":true,"multiple":false,"targetEntities":["Post"],"semantics":[],"cardinality":"One-to-One"},{"name":"author","displayName":"Comment Author","description":"Registered user who wrote this comment","required":false,"multiple":false,"targetEntities":["user"],"semantics":["Semantic#ResourceOwnerIdentifier"],"cardinality":"One-to-One"},{"name":"parent_comment","displayName":"Parent Comment","description":"Parent comment for replies","required":false,"multiple":false,"targetEntities":["comment"],"semantics":[],"cardinality":"One-to-One"}],"semantics":[]}]}]},{"name":"Analytics","displayName":"Analytics & Tracking","description":"Analytics, metrics, and performance tracking","modelCount":1,"entityCount":1,"models":[{"name":"Analytics","displayName":"Content Analytics","description":"Content performance and user engagement metrics","entityCount":1,"entities":[{"name":"page_view","displayName":"Page View","description":"Individual page view tracking record","propertyCount":7,"associationCount":2,"properties":[{"name":"id","displayName":"Page View ID","description":"Unique identifier for the page view","type":"string","primary":true,"readOnly":true,"semantics":[]},{"name":"path","displayName":"Page Path","description":"URL path of the viewed page","type":"string","required":true,"semantics":[]},{"name":"referrer","displayName":"Referrer","description":"Referring URL","type":"string","semantics":["Semantic#URL"]},{"name":"ip_address","displayName":"IP Address","description":"Visitor IP address","type":"string","semantics":["Semantic#ClientIPAddress"]},{"name":"user_agent","displayName":"User Agent","description":"Browser user agent string","type":"string","semantics":[]},{"name":"session_id","displayName":"Session ID","description":"Visitor session identifier","type":"string","semantics":[]},{"name":"viewed_at","displayName":"Viewed At","description":"When the page was viewed","type":"datetime","readOnly":true,"semantics":["Semantic#CreatedTimestamp"]}],"associations":[{"name":"post","displayName":"Viewed Post","description":"Post that was viewed (if applicable)","required":false,"multiple":false,"targetEntities":["Post"],"semantics":[],"cardinality":"One-to-One"},{"name":"publication","displayName":"Publication","description":"Publication this page view belongs to","required":true,"multiple":false,"targetEntities":["publication"],"semantics":[],"cardinality":"One-to-One"}],"semantics":[]}]}]},{"name":"MediaManagement","displayName":"Media Management","description":"Image, video, and file upload management","modelCount":1,"entityCount":1,"models":[{"name":"Media","displayName":"Media Library","description":"Uploaded files, images, and media assets","entityCount":1,"entities":[{"name":"media_file","displayName":"Media File","description":"Uploaded media files (images, videos, documents)","propertyCount":10,"associationCount":2,"properties":[{"name":"id","displayName":"Media File ID","description":"Unique identifier for the media file","type":"string","primary":true,"readOnly":true,"semantics":[]},{"name":"filename","displayName":"File Name","description":"Original filename of the uploaded file","type":"string","required":true,"semantics":[]},{"name":"storage_key","displayName":"Storage Key","description":"Unique storage key for the file","type":"string","required":true,"unique":true,"semantics":[]},{"name":"url","displayName":"File URL","description":"Public URL to access the file","type":"string","required":true,"semantics":["Semantic#URL"]},{"name":"mime_type","displayName":"MIME Type","description":"MIME type of the file","type":"string","required":true,"semantics":[]},{"name":"file_size","displayName":"File Size","description":"File size in bytes","type":"number","required":true,"semantics":[]},{"name":"width","displayName":"Image Width","description":"Width in pixels (for images)","type":"number","required":true,"semantics":[]},{"name":"height","displayName":"Image Height","description":"Height in pixels (for images)","type":"number","required":true,"semantics":[]},{"name":"alt_text","displayName":"Alt Text","description":"Alternative text for accessibility","type":"string","semantics":[]},{"name":"uploaded_at","displayName":"Uploaded At","description":"When the file was uploaded","type":"datetime","readOnly":true,"semantics":["Semantic#CreatedTimestamp"]}],"associations":[{"name":"uploader","displayName":"File Uploader","description":"User who uploaded this file","required":true,"multiple":false,"targetEntities":["user"],"semantics":["Semantic#ResourceOwnerIdentifier"],"cardinality":"One-to-One"},{"name":"publication","displayName":"Publication","description":"Publication this media file belongs to","required":true,"multiple":false,"targetEntities":["publication"],"semantics":[],"cardinality":"One-to-One"}],"semantics":[]}]}]}]}}
|
|
1
|
+
{"id":"blog-publishing-platform","name":"Blog Publishing Platform","description":"A comprehensive content management and publishing platform for blogs, magazines, and digital publications. Includes content management, user roles, publishing workflow, and social features.","createdAt":"2025-07-27T21:14:57.328Z","updatedAt":"2025-07-27T21:14:57.328Z","version":"1.0.0","author":"API Now! Core Team","tags":["blog","cms","publishing","content","media","editorial"],"structure":{"domain":{"name":"Blog Publishing Platform","description":"A comprehensive content management and publishing platform for blogs, magazines, and digital publications","totalEntities":8,"totalProperties":70,"totalAssociations":14},"namespaces":[{"name":"ContentManagement","displayName":"Content Management","description":"Core content creation, editing, and organization features","modelCount":2,"entityCount":4,"models":[{"name":"Publications","displayName":"Publications Management","description":"Individual blogs, magazines, or publications within the platform","entityCount":1,"entities":[{"name":"publication","displayName":"Publication","description":"Individual blog or publication site","propertyCount":8,"associationCount":0,"properties":[{"name":"id","displayName":"Publication ID","description":"Unique identifier for the publication","type":"string","primary":true,"readOnly":true,"semantics":[]},{"name":"name","displayName":"Publication Name","description":"Display name of the publication","type":"string","semantics":["Semantic#Title"]},{"name":"slug","displayName":"URL Slug","description":"URL-friendly identifier for the publication","type":"string","required":true,"unique":true,"semantics":["Semantic#PublicUniqueName"]},{"name":"description","displayName":"Description","description":"Publication description and tagline","type":"string","semantics":["Semantic#Description"]},{"name":"domain","displayName":"Custom Domain","description":"Custom domain name for the publication","type":"string","semantics":[]},{"name":"logo_url","displayName":"Logo URL","description":"URL to publication logo image","type":"string","semantics":["Semantic#ImageURL"]},{"name":"status","displayName":"Publication Status","description":"Current status of the publication","type":"string","required":true,"semantics":["Semantic#Status"],"enumValues":["active","suspended","archived"],"defaultValue":"active"},{"name":"created_at","displayName":"Created At","description":"When the publication was created","type":"datetime","readOnly":true,"semantics":["Semantic#CreatedTimestamp"]}],"associations":[],"semantics":[]}]},{"name":"Content","displayName":"Content Management","description":"Posts, pages, and other content types","entityCount":3,"entities":[{"name":"category","displayName":"Content Category","description":"Content categorization for organization and navigation","propertyCount":5,"associationCount":2,"properties":[{"name":"id","displayName":"Category ID","description":"Unique identifier for the category","type":"string","primary":true,"readOnly":true,"semantics":[]},{"name":"name","displayName":"Category Name","description":"Display name of the category","type":"string","semantics":["Semantic#Title"]},{"name":"slug","displayName":"URL Slug","description":"URL-friendly identifier for the category","type":"string","required":true,"unique":true,"semantics":["Semantic#PublicUniqueName"]},{"name":"description","displayName":"Description","description":"Category description","type":"string","semantics":["Semantic#Description"]},{"name":"color","displayName":"Color","description":"Theme color for the category","type":"string","semantics":[]}],"associations":[{"name":"publication","displayName":"Publication","description":"Publication this category belongs to","required":true,"multiple":false,"targetEntities":["publication"],"semantics":[],"cardinality":"One-to-One"},{"name":"parentCategory","displayName":"Parent Category","description":"Parent category for hierarchical organization","required":false,"multiple":false,"targetEntities":["category"],"semantics":[],"cardinality":"One-to-One"}],"semantics":[]},{"name":"Tag","displayName":"Content Tag","description":"Tags for flexible content labeling and discovery","propertyCount":3,"associationCount":1,"properties":[{"name":"id","displayName":"Tag ID","description":"Unique identifier for the tag","type":"string","primary":true,"readOnly":true,"semantics":[]},{"name":"name","displayName":"Tag Name","description":"Display name of the tag","type":"string","semantics":["Semantic#Title"]},{"name":"slug","displayName":"URL Slug","description":"URL-friendly identifier for the tag","type":"string","required":true,"unique":true,"semantics":["Semantic#PublicUniqueName"]}],"associations":[{"name":"publication","displayName":"Publication","description":"Publication this tag belongs to","required":true,"multiple":false,"targetEntities":["publication"],"semantics":[],"cardinality":"One-to-One"}],"semantics":[]},{"name":"Post","displayName":"Blog Post","description":"Individual blog posts and articles","propertyCount":16,"associationCount":4,"properties":[{"name":"id","displayName":"Post ID","description":"Unique identifier for the post","type":"string","primary":true,"readOnly":true,"semantics":[]},{"name":"title","displayName":"Post Title","description":"Title of the blog post","type":"string","required":true,"semantics":["Semantic#Title"]},{"name":"slug","displayName":"URL Slug","description":"URL-friendly identifier for the post","type":"string","required":true,"unique":true,"semantics":["Semantic#PublicUniqueName"]},{"name":"excerpt","displayName":"Excerpt","description":"Brief summary or excerpt of the post","type":"string","semantics":["Semantic#Summary"]},{"name":"content","displayName":"Content","description":"Full content of the post in HTML or Markdown","type":"string","required":true,"semantics":["Semantic#HTML"]},{"name":"content_format","displayName":"Content Format","description":"Format of the content (HTML, Markdown, etc.)","type":"string","required":true,"semantics":[],"enumValues":["html","markdown","richtext"],"defaultValue":"markdown"},{"name":"featured_image_url","displayName":"Featured Image URL","description":"URL to featured image","type":"string","semantics":["Semantic#ImageURL"]},{"name":"status","displayName":"Post Status","description":"Current publishing status of the post","type":"string","required":true,"semantics":["Semantic#Status"],"enumValues":["draft","pending_review","scheduled","published","archived"],"defaultValue":"draft"},{"name":"published_at","displayName":"Published At","description":"When the post was published","type":"datetime","readOnly":true,"semantics":["Semantic#CreatedTimestamp"]},{"name":"scheduled_at","displayName":"Scheduled At","description":"When the post is scheduled to be published","type":"datetime","semantics":[]},{"name":"view_count","displayName":"View Count","description":"Number of times the post has been viewed","type":"number","required":true,"semantics":[]},{"name":"reading_time","displayName":"Reading Time","description":"Estimated reading time in minutes","type":"number","readOnly":true,"semantics":["Semantic#Calculated"]},{"name":"word_count","displayName":"Word Count","description":"Number of words in the post content","type":"number","readOnly":true,"semantics":["Semantic#Calculated"]},{"name":"meta_title","displayName":"Meta Title","description":"SEO meta title","type":"string","semantics":[]},{"name":"meta_description","displayName":"Meta Description","description":"SEO meta description","type":"string","semantics":[]},{"name":"updated_at","displayName":"Updated At","description":"When the post was last updated","type":"datetime","readOnly":true,"semantics":["Semantic#UpdatedTimestamp"]}],"associations":[{"name":"publication","displayName":"Publication","description":"Publication this post belongs to","required":true,"multiple":false,"targetEntities":["publication"],"semantics":[],"cardinality":"One-to-One"},{"name":"categories","displayName":"Post Categories","description":"Categories this post belongs to","required":false,"multiple":true,"targetEntities":["category"],"semantics":["Semantic#Categories"],"cardinality":"One-to-Many"},{"name":"tags","displayName":"Post Tags","description":"Tags associated with this post","required":false,"multiple":true,"targetEntities":["Tag"],"semantics":["Semantic#Tags"],"cardinality":"One-to-Many"},{"name":"author","displayName":"Post Author","description":"Author who wrote this post","required":true,"multiple":false,"targetEntities":["user"],"semantics":["Semantic#ResourceOwnerIdentifier"],"cardinality":"One-to-One"}],"semantics":[]}]}]},{"name":"UserManagement","displayName":"User Management","description":"Authors, editors, subscribers, and user roles management","modelCount":1,"entityCount":1,"models":[{"name":"Users","displayName":"User Management","description":"User accounts and authentication","entityCount":1,"entities":[{"name":"user","displayName":"User Account","description":"User account for authors, editors, and subscribers","propertyCount":14,"associationCount":0,"properties":[{"name":"id","displayName":"User ID","description":"Unique identifier for the user","type":"string","primary":true,"readOnly":true,"semantics":[]},{"name":"email","displayName":"Email Address","description":"User email address for login and communication","type":"string","required":true,"semantics":["Semantic#Email"]},{"name":"password","displayName":"Password","description":"Encrypted password for authentication","type":"string","required":true,"semantics":["Semantic#Password"]},{"name":"username","displayName":"Username","description":"Unique username for the user","type":"string","required":true,"unique":true,"semantics":["Semantic#PublicUniqueName"]},{"name":"display_name","displayName":"Display Name","description":"Public display name for the user","type":"string","required":true,"semantics":[]},{"name":"first_name","displayName":"First Name","description":"User first name","type":"string","semantics":[]},{"name":"last_name","displayName":"Last Name","description":"User last name","type":"string","semantics":[]},{"name":"bio","displayName":"Biography","description":"User biography and description","type":"string","semantics":["Semantic#Description"]},{"name":"avatar_url","displayName":"Avatar URL","description":"URL to user profile picture","type":"string","semantics":["Semantic#ImageURL"]},{"name":"website","displayName":"Website","description":"User personal website URL","type":"string","semantics":["Semantic#URL"]},{"name":"role","displayName":"User Role","description":"User role for permission management","type":"string","required":true,"semantics":["Semantic#UserRole"],"enumValues":["subscriber","author","editor","admin","super_admin"],"defaultValue":"subscriber"},{"name":"status","displayName":"User Status","description":"Current status of the user account","type":"string","required":true,"semantics":["Semantic#Status"],"enumValues":["active","inactive","suspended","pending_verification"],"defaultValue":"active"},{"name":"emailVerified","displayName":"Email Verified","description":"Whether the user has verified their email","type":"boolean","required":true,"semantics":[],"defaultValue":"false"},{"name":"created_at","displayName":"Created At","description":"When the user account was created","type":"datetime","readOnly":true,"semantics":["Semantic#CreatedTimestamp"]}],"associations":[],"semantics":["Semantic#User"]}]}]},{"name":"SocialFeatures","displayName":"Social Features","description":"Comments, likes, shares, and social interactions","modelCount":1,"entityCount":1,"models":[{"name":"Comments","displayName":"Comment System","description":"Post comments and replies","entityCount":1,"entities":[{"name":"comment","displayName":"Comment","description":"User comments on posts","propertyCount":7,"associationCount":3,"properties":[{"name":"id","displayName":"Comment ID","description":"Unique identifier for the comment","type":"string","primary":true,"readOnly":true,"semantics":[]},{"name":"content","displayName":"Comment Content","description":"Content of the comment","type":"string","required":true,"semantics":[]},{"name":"author_name","displayName":"Author Name","description":"Name of the comment author (for guest comments)","type":"string","semantics":[]},{"name":"author_email","displayName":"Author Email","description":"Email of the comment author (for guest comments)","type":"string","required":true,"semantics":["Semantic#Email"]},{"name":"status","displayName":"Comment Status","description":"Moderation status of the comment","type":"string","required":true,"semantics":["Semantic#Status"],"enumValues":["pending","approved","rejected","spam"],"defaultValue":"pending"},{"name":"user_agent","displayName":"User Agent","description":"Browser user agent string","type":"string","semantics":[]},{"name":"created_at","displayName":"Created At","description":"When the comment was created","type":"datetime","readOnly":true,"semantics":["Semantic#CreatedTimestamp"]}],"associations":[{"name":"post","displayName":"Post","description":"Post this comment belongs to","required":true,"multiple":false,"targetEntities":["Post"],"semantics":[],"cardinality":"One-to-One"},{"name":"author","displayName":"Comment Author","description":"Registered user who wrote this comment","required":false,"multiple":false,"targetEntities":["user"],"semantics":["Semantic#ResourceOwnerIdentifier"],"cardinality":"One-to-One"},{"name":"parent_comment","displayName":"Parent Comment","description":"Parent comment for replies","required":false,"multiple":false,"targetEntities":["comment"],"semantics":[],"cardinality":"One-to-One"}],"semantics":[]}]}]},{"name":"Analytics","displayName":"Analytics & Tracking","description":"Analytics, metrics, and performance tracking","modelCount":1,"entityCount":1,"models":[{"name":"Analytics","displayName":"Content Analytics","description":"Content performance and user engagement metrics","entityCount":1,"entities":[{"name":"page_view","displayName":"Page View","description":"Individual page view tracking record","propertyCount":7,"associationCount":2,"properties":[{"name":"id","displayName":"Page View ID","description":"Unique identifier for the page view","type":"string","primary":true,"readOnly":true,"semantics":[]},{"name":"path","displayName":"Page Path","description":"URL path of the viewed page","type":"string","required":true,"semantics":[]},{"name":"referrer","displayName":"Referrer","description":"Referring URL","type":"string","semantics":["Semantic#URL"]},{"name":"ip_address","displayName":"IP Address","description":"Visitor IP address","type":"string","semantics":["Semantic#ClientIPAddress"]},{"name":"user_agent","displayName":"User Agent","description":"Browser user agent string","type":"string","semantics":[]},{"name":"session_id","displayName":"Session ID","description":"Visitor session identifier","type":"string","semantics":[]},{"name":"viewed_at","displayName":"Viewed At","description":"When the page was viewed","type":"datetime","readOnly":true,"semantics":["Semantic#CreatedTimestamp"]}],"associations":[{"name":"post","displayName":"Viewed Post","description":"Post that was viewed (if applicable)","required":false,"multiple":false,"targetEntities":["Post"],"semantics":[],"cardinality":"One-to-One"},{"name":"publication","displayName":"Publication","description":"Publication this page view belongs to","required":true,"multiple":false,"targetEntities":["publication"],"semantics":[],"cardinality":"One-to-One"}],"semantics":[]}]}]},{"name":"MediaManagement","displayName":"Media Management","description":"Image, video, and file upload management","modelCount":1,"entityCount":1,"models":[{"name":"Media","displayName":"Media Library","description":"Uploaded files, images, and media assets","entityCount":1,"entities":[{"name":"media_file","displayName":"Media File","description":"Uploaded media files (images, videos, documents)","propertyCount":10,"associationCount":2,"properties":[{"name":"id","displayName":"Media File ID","description":"Unique identifier for the media file","type":"string","primary":true,"readOnly":true,"semantics":[]},{"name":"filename","displayName":"File Name","description":"Original filename of the uploaded file","type":"string","required":true,"semantics":[]},{"name":"storage_key","displayName":"Storage Key","description":"Unique storage key for the file","type":"string","required":true,"unique":true,"semantics":[]},{"name":"url","displayName":"File URL","description":"Public URL to access the file","type":"string","required":true,"semantics":["Semantic#URL"]},{"name":"mime_type","displayName":"MIME Type","description":"MIME type of the file","type":"string","required":true,"semantics":[]},{"name":"file_size","displayName":"File Size","description":"File size in bytes","type":"number","required":true,"semantics":[]},{"name":"width","displayName":"Image Width","description":"Width in pixels (for images)","type":"number","required":true,"semantics":[]},{"name":"height","displayName":"Image Height","description":"Height in pixels (for images)","type":"number","required":true,"semantics":[]},{"name":"alt_text","displayName":"Alt Text","description":"Alternative text for accessibility","type":"string","semantics":[]},{"name":"uploaded_at","displayName":"Uploaded At","description":"When the file was uploaded","type":"datetime","readOnly":true,"semantics":["Semantic#CreatedTimestamp"]}],"associations":[{"name":"uploader","displayName":"File Uploader","description":"User who uploaded this file","required":true,"multiple":false,"targetEntities":["user"],"semantics":["Semantic#ResourceOwnerIdentifier"],"cardinality":"One-to-One"},{"name":"publication","displayName":"Publication","description":"Publication this media file belongs to","required":true,"multiple":false,"targetEntities":["publication"],"semantics":[],"cardinality":"One-to-One"}],"semantics":[]}]}]}]}}
|