@declaro/data 2.0.0-beta.14 → 2.0.0-beta.140
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/{LICENSE → LICENSE.md} +1 -1
- package/README.md +0 -0
- package/dist/browser/index.js +26 -0
- package/dist/browser/index.js.map +93 -0
- package/dist/node/index.cjs +13372 -0
- package/dist/node/index.cjs.map +93 -0
- package/dist/node/index.js +13351 -0
- package/dist/node/index.js.map +93 -0
- package/dist/ts/application/model-controller.d.ts +60 -0
- package/dist/ts/application/model-controller.d.ts.map +1 -0
- package/dist/ts/application/model-controller.test.d.ts +2 -0
- package/dist/ts/application/model-controller.test.d.ts.map +1 -0
- package/dist/ts/application/read-only-model-controller.d.ts +24 -0
- package/dist/ts/application/read-only-model-controller.d.ts.map +1 -0
- package/dist/ts/application/read-only-model-controller.test.d.ts +2 -0
- package/dist/ts/application/read-only-model-controller.test.d.ts.map +1 -0
- package/dist/ts/domain/events/domain-event.d.ts +41 -0
- package/dist/ts/domain/events/domain-event.d.ts.map +1 -0
- package/dist/ts/domain/events/domain-event.test.d.ts +2 -0
- package/dist/ts/domain/events/domain-event.test.d.ts.map +1 -0
- package/dist/ts/domain/events/event-types.d.ts +37 -0
- package/dist/ts/domain/events/event-types.d.ts.map +1 -0
- package/dist/ts/domain/events/mutation-event.d.ts +41 -0
- package/dist/ts/domain/events/mutation-event.d.ts.map +1 -0
- package/dist/ts/domain/events/mutation-event.test.d.ts +2 -0
- package/dist/ts/domain/events/mutation-event.test.d.ts.map +1 -0
- package/dist/ts/domain/events/query-event.d.ts +8 -0
- package/dist/ts/domain/events/query-event.d.ts.map +1 -0
- package/dist/ts/domain/events/query-event.test.d.ts +2 -0
- package/dist/ts/domain/events/query-event.test.d.ts.map +1 -0
- package/dist/ts/domain/events/request-event.d.ts +26 -0
- package/dist/ts/domain/events/request-event.d.ts.map +1 -0
- package/dist/ts/domain/events/request-event.test.d.ts +2 -0
- package/dist/ts/domain/events/request-event.test.d.ts.map +1 -0
- package/dist/ts/domain/interfaces/repository.d.ts +110 -0
- package/dist/ts/domain/interfaces/repository.d.ts.map +1 -0
- package/dist/ts/domain/models/pagination.d.ts +28 -0
- package/dist/ts/domain/models/pagination.d.ts.map +1 -0
- package/dist/ts/domain/services/base-model-service.d.ts +23 -0
- package/dist/ts/domain/services/base-model-service.d.ts.map +1 -0
- package/dist/ts/domain/services/model-service-args.d.ts +9 -0
- package/dist/ts/domain/services/model-service-args.d.ts.map +1 -0
- package/dist/ts/domain/services/model-service.d.ts +99 -0
- package/dist/ts/domain/services/model-service.d.ts.map +1 -0
- package/dist/ts/domain/services/model-service.normalization.test.d.ts +2 -0
- package/dist/ts/domain/services/model-service.normalization.test.d.ts.map +1 -0
- package/dist/ts/domain/services/model-service.test.d.ts +2 -0
- package/dist/ts/domain/services/model-service.test.d.ts.map +1 -0
- package/dist/ts/domain/services/read-only-model-service.d.ts +90 -0
- package/dist/ts/domain/services/read-only-model-service.d.ts.map +1 -0
- package/dist/ts/domain/services/read-only-model-service.test.d.ts +2 -0
- package/dist/ts/domain/services/read-only-model-service.test.d.ts.map +1 -0
- package/dist/ts/index.d.ts +18 -0
- package/dist/ts/index.d.ts.map +1 -0
- package/dist/ts/shared/utils/schema-inference.d.ts +23 -0
- package/dist/ts/shared/utils/schema-inference.d.ts.map +1 -0
- package/dist/ts/shared/utils/schema-inheritance.d.ts +24 -0
- package/dist/ts/shared/utils/schema-inheritance.d.ts.map +1 -0
- package/dist/ts/shared/utils/schema-inheritance.test.d.ts +2 -0
- package/dist/ts/shared/utils/schema-inheritance.test.d.ts.map +1 -0
- package/dist/ts/shared/utils/test/animal-schema.d.ts +57 -0
- package/dist/ts/shared/utils/test/animal-schema.d.ts.map +1 -0
- package/dist/ts/shared/utils/test/animal-trait-schema.d.ts +55 -0
- package/dist/ts/shared/utils/test/animal-trait-schema.d.ts.map +1 -0
- package/dist/ts/shared/utils/test/elephant-schema.d.ts +30 -0
- package/dist/ts/shared/utils/test/elephant-schema.d.ts.map +1 -0
- package/dist/ts/shared/utils/test/elephant-trait-schema.d.ts +26 -0
- package/dist/ts/shared/utils/test/elephant-trait-schema.d.ts.map +1 -0
- package/dist/ts/test/mock/models/mock-book-models.d.ts +42 -0
- package/dist/ts/test/mock/models/mock-book-models.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.assign.test.d.ts +2 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.assign.test.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.basic.test.d.ts +2 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.basic.test.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.bulk-upsert.test.d.ts +2 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.bulk-upsert.test.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.count.test.d.ts +2 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.count.test.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts +62 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.search.test.d.ts +2 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.search.test.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.trash.test.d.ts +2 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.trash.test.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.upsert.test.d.ts +2 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.upsert.test.d.ts.map +1 -0
- package/package.json +46 -42
- package/src/application/model-controller.test.ts +694 -0
- package/src/application/model-controller.ts +186 -0
- package/src/application/read-only-model-controller.test.ts +335 -0
- package/src/application/read-only-model-controller.ts +79 -0
- package/src/domain/events/domain-event.test.ts +82 -0
- package/src/domain/events/domain-event.ts +69 -0
- package/src/domain/events/event-types.ts +37 -0
- package/src/domain/events/mutation-event.test.ts +390 -0
- package/src/domain/events/mutation-event.ts +53 -0
- package/src/domain/events/query-event.test.ts +228 -0
- package/src/domain/events/query-event.ts +14 -0
- package/src/domain/events/request-event.test.ts +38 -0
- package/src/domain/events/request-event.ts +47 -0
- package/src/domain/interfaces/repository.ts +136 -0
- package/src/domain/models/pagination.ts +28 -0
- package/src/domain/services/base-model-service.ts +54 -0
- package/src/domain/services/model-service-args.ts +9 -0
- package/src/domain/services/model-service.normalization.test.ts +704 -0
- package/src/domain/services/model-service.test.ts +1616 -0
- package/src/domain/services/model-service.ts +597 -0
- package/src/domain/services/read-only-model-service.test.ts +1130 -0
- package/src/domain/services/read-only-model-service.ts +211 -0
- package/src/index.ts +17 -4
- package/src/shared/utils/schema-inference.ts +26 -0
- package/src/shared/utils/schema-inheritance.test.ts +295 -0
- package/src/shared/utils/schema-inheritance.ts +28 -0
- package/src/shared/utils/test/animal-schema.ts +46 -0
- package/src/shared/utils/test/animal-trait-schema.ts +45 -0
- package/src/shared/utils/test/elephant-schema.ts +58 -0
- package/src/shared/utils/test/elephant-trait-schema.ts +53 -0
- package/src/test/mock/models/mock-book-models.ts +78 -0
- package/src/test/mock/repositories/mock-memory-repository.assign.test.ts +215 -0
- package/src/test/mock/repositories/mock-memory-repository.basic.test.ts +129 -0
- package/src/test/mock/repositories/mock-memory-repository.bulk-upsert.test.ts +159 -0
- package/src/test/mock/repositories/mock-memory-repository.count.test.ts +98 -0
- package/src/test/mock/repositories/mock-memory-repository.search.test.ts +265 -0
- package/src/test/mock/repositories/mock-memory-repository.trash.test.ts +736 -0
- package/src/test/mock/repositories/mock-memory-repository.ts +401 -0
- package/src/test/mock/repositories/mock-memory-repository.upsert.test.ts +108 -0
- package/dist/databaseConnection.d.ts +0 -24
- package/dist/datastoreAbstract.d.ts +0 -37
- package/dist/declaro-data.cjs +0 -1
- package/dist/declaro-data.mjs +0 -250
- package/dist/hydrateEntity.d.ts +0 -8
- package/dist/index.d.ts +0 -4
- package/dist/serverConnection.d.ts +0 -15
- package/dist/trackedStatus.d.ts +0 -15
- package/src/databaseConnection.ts +0 -137
- package/src/datastoreAbstract.ts +0 -190
- package/src/hydrateEntity.ts +0 -36
- package/src/placeholder.test.ts +0 -7
- package/src/serverConnection.ts +0 -74
- package/src/trackedStatus.ts +0 -35
- package/tsconfig.json +0 -10
- package/vite.config.ts +0 -23
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { AnyModelSchema, Model } from '@declaro/core'
|
|
2
|
+
import type {
|
|
3
|
+
InferDetail,
|
|
4
|
+
InferFilters,
|
|
5
|
+
InferLookup,
|
|
6
|
+
InferSearchResults,
|
|
7
|
+
InferSort,
|
|
8
|
+
InferSummary,
|
|
9
|
+
} from '../../shared/utils/schema-inference'
|
|
10
|
+
import { ModelQueryEvent } from '../events/event-types'
|
|
11
|
+
import { QueryEvent } from '../events/query-event'
|
|
12
|
+
import { BaseModelService, type IActionOptions } from './base-model-service'
|
|
13
|
+
import type { IPaginationInput } from '../models/pagination'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Options for loading records.
|
|
17
|
+
*/
|
|
18
|
+
export interface ILoadOptions extends IActionOptions {
|
|
19
|
+
/**
|
|
20
|
+
* If true, only removed (soft-deleted) records will be returned.
|
|
21
|
+
*/
|
|
22
|
+
removedOnly?: boolean
|
|
23
|
+
/**
|
|
24
|
+
* If true, both removed and non-removed records will be returned.
|
|
25
|
+
*/
|
|
26
|
+
includeRemoved?: boolean
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* If true, skips dispatching events for this action.
|
|
30
|
+
*/
|
|
31
|
+
doNotDispatchEvents?: boolean
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* If true, bypasses any caching mechanism and forces fresh data retrieval from the repository.
|
|
35
|
+
*
|
|
36
|
+
* Use with caution, as this may have performance implications. This option is typically used in scenarios where data consistency is critical and the latest data must be retrieved, such as after a known update or in real-time applications.
|
|
37
|
+
*/
|
|
38
|
+
noCache?: boolean
|
|
39
|
+
}
|
|
40
|
+
export interface ISearchOptions<TSchema extends AnyModelSchema> extends IActionOptions {
|
|
41
|
+
pagination?: IPaginationInput
|
|
42
|
+
sort?: InferSort<TSchema>
|
|
43
|
+
/**
|
|
44
|
+
* If true, only removed (soft-deleted) records will be returned.
|
|
45
|
+
*/
|
|
46
|
+
removedOnly?: boolean
|
|
47
|
+
/**
|
|
48
|
+
* If true, both removed and non-removed records will be returned.
|
|
49
|
+
*/
|
|
50
|
+
includeRemoved?: boolean
|
|
51
|
+
/**
|
|
52
|
+
* If true, skips dispatching events for this action.
|
|
53
|
+
*/
|
|
54
|
+
doNotDispatchEvents?: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class ReadOnlyModelService<TSchema extends AnyModelSchema> extends BaseModelService<TSchema> {
|
|
58
|
+
/**
|
|
59
|
+
* Normalize the detail data to match the expected schema.
|
|
60
|
+
* WARNING: This method is called once per detail in load operations.
|
|
61
|
+
* Any intensive operations or queries should be avoided here, and done via bulk operations in the respective methods such as `loadMany` instead.
|
|
62
|
+
* @param detail The detail data to normalize.
|
|
63
|
+
* @returns The normalized detail data.
|
|
64
|
+
*/
|
|
65
|
+
async normalizeDetail(detail: InferDetail<TSchema>): Promise<InferDetail<TSchema>> {
|
|
66
|
+
return detail
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Normalize the summary data to match the expected schema.
|
|
71
|
+
* WARNING: This method is called once per summary in search results, often in parallel.
|
|
72
|
+
* Any intensive operations or queries should be avoided here, and done via bulk operations in the respective methods such as `search` instead.
|
|
73
|
+
*
|
|
74
|
+
* @param summary The summary data to normalize.
|
|
75
|
+
* @returns The normalized summary data.
|
|
76
|
+
*/
|
|
77
|
+
async normalizeSummary(summary: InferSummary<TSchema>): Promise<InferSummary<TSchema>> {
|
|
78
|
+
return summary
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Load a single record by its lookup criteria.
|
|
83
|
+
* @param lookup The lookup criteria to find the record.
|
|
84
|
+
* @param options Additional options for the load operation.
|
|
85
|
+
* @returns The loaded record details.
|
|
86
|
+
*/
|
|
87
|
+
async load(lookup: InferLookup<TSchema>, options?: ILoadOptions): Promise<InferDetail<TSchema>> {
|
|
88
|
+
// Emit the before load event
|
|
89
|
+
if (!options?.doNotDispatchEvents) {
|
|
90
|
+
const beforeLoadEvent = new QueryEvent<InferDetail<TSchema>, InferLookup<TSchema>>(
|
|
91
|
+
this.getDescriptor(ModelQueryEvent.BeforeLoad, options?.scope),
|
|
92
|
+
lookup,
|
|
93
|
+
)
|
|
94
|
+
await this.emitter.emitAsync(beforeLoadEvent)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Load the details from the repository
|
|
98
|
+
const details = await this.repository.load(lookup, options)
|
|
99
|
+
|
|
100
|
+
// Emit the after load event
|
|
101
|
+
if (!options?.doNotDispatchEvents) {
|
|
102
|
+
const afterLoadEvent = new QueryEvent<InferDetail<TSchema>, InferLookup<TSchema>>(
|
|
103
|
+
this.getDescriptor(ModelQueryEvent.AfterLoad, options?.scope),
|
|
104
|
+
lookup,
|
|
105
|
+
).setResult(details)
|
|
106
|
+
await this.emitter.emitAsync(afterLoadEvent)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return await this.normalizeDetail(details)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Load multiple records by their lookup criteria.
|
|
114
|
+
* @param lookups The lookup criteria to find the records.
|
|
115
|
+
* @param options Additional options for the load operation.
|
|
116
|
+
* @returns An array of loaded record details.
|
|
117
|
+
*/
|
|
118
|
+
async loadMany(lookups: InferLookup<TSchema>[], options?: ILoadOptions): Promise<InferDetail<TSchema>[]> {
|
|
119
|
+
// Emit the before load many event
|
|
120
|
+
if (!options?.doNotDispatchEvents) {
|
|
121
|
+
const beforeLoadManyEvent = new QueryEvent<InferDetail<TSchema>[], InferLookup<TSchema>[]>(
|
|
122
|
+
this.getDescriptor(ModelQueryEvent.BeforeLoadMany, options?.scope),
|
|
123
|
+
lookups,
|
|
124
|
+
)
|
|
125
|
+
await this.emitter.emitAsync(beforeLoadManyEvent)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Load the details from the repository
|
|
129
|
+
const details = await this.repository.loadMany(lookups, options)
|
|
130
|
+
|
|
131
|
+
// Emit the after load many event
|
|
132
|
+
if (!options?.doNotDispatchEvents) {
|
|
133
|
+
const afterLoadManyEvent = new QueryEvent<InferDetail<TSchema>[], InferLookup<TSchema>[]>(
|
|
134
|
+
this.getDescriptor(ModelQueryEvent.AfterLoadMany, options?.scope),
|
|
135
|
+
lookups,
|
|
136
|
+
).setResult(details)
|
|
137
|
+
await this.emitter.emitAsync(afterLoadManyEvent)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return await Promise.all(details.map((detail) => this.normalizeDetail(detail)))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Search for records matching the given filters.
|
|
145
|
+
* @param filters The filters to apply to the search.
|
|
146
|
+
* @param options Additional options for the search operation.
|
|
147
|
+
* @returns The search results.
|
|
148
|
+
*/
|
|
149
|
+
async search(
|
|
150
|
+
filters: InferFilters<TSchema>,
|
|
151
|
+
options?: ISearchOptions<TSchema>,
|
|
152
|
+
): Promise<InferSearchResults<TSchema>> {
|
|
153
|
+
// Emit the before search event
|
|
154
|
+
if (!options?.doNotDispatchEvents) {
|
|
155
|
+
const beforeSearchEvent = new QueryEvent<InferSearchResults<TSchema>, InferFilters<TSchema>>(
|
|
156
|
+
this.getDescriptor(ModelQueryEvent.BeforeSearch, options?.scope),
|
|
157
|
+
filters,
|
|
158
|
+
)
|
|
159
|
+
await this.emitter.emitAsync(beforeSearchEvent)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Search the repository with the provided filters
|
|
163
|
+
const results = await this.repository.search(filters, options)
|
|
164
|
+
|
|
165
|
+
// Emit the after search event
|
|
166
|
+
if (!options?.doNotDispatchEvents) {
|
|
167
|
+
const afterSearchEvent = new QueryEvent<InferSearchResults<TSchema>, InferFilters<TSchema>>(
|
|
168
|
+
this.getDescriptor(ModelQueryEvent.AfterSearch, options?.scope),
|
|
169
|
+
filters,
|
|
170
|
+
).setResult(results)
|
|
171
|
+
await this.emitter.emitAsync(afterSearchEvent)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Return the search results
|
|
175
|
+
return {
|
|
176
|
+
...results,
|
|
177
|
+
results: await Promise.all(results.results.map((summary) => this.normalizeSummary(summary))),
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Count the number of records matching the given filters.
|
|
183
|
+
* @param filters The filters to apply to the count operation.
|
|
184
|
+
* @returns The count of matching records.
|
|
185
|
+
*/
|
|
186
|
+
async count(filters: InferFilters<TSchema>, options?: ISearchOptions<TSchema>): Promise<number> {
|
|
187
|
+
// Emit the before count event
|
|
188
|
+
if (!options?.doNotDispatchEvents) {
|
|
189
|
+
const beforeCountEvent = new QueryEvent<number, InferFilters<TSchema>>(
|
|
190
|
+
this.getDescriptor(ModelQueryEvent.BeforeCount, options?.scope),
|
|
191
|
+
filters,
|
|
192
|
+
)
|
|
193
|
+
await this.emitter.emitAsync(beforeCountEvent)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Count the records in the repository
|
|
197
|
+
const count = await this.repository.count(filters, options)
|
|
198
|
+
|
|
199
|
+
// Emit the after count event
|
|
200
|
+
if (!options?.doNotDispatchEvents) {
|
|
201
|
+
const afterCountEvent = new QueryEvent<number, InferFilters<TSchema>>(
|
|
202
|
+
this.getDescriptor(ModelQueryEvent.AfterCount, options?.scope),
|
|
203
|
+
filters,
|
|
204
|
+
).setResult(count)
|
|
205
|
+
await this.emitter.emitAsync(afterCountEvent)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Return the count
|
|
209
|
+
return count
|
|
210
|
+
}
|
|
211
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
export * from './
|
|
2
|
-
export * from './
|
|
3
|
-
export * from './
|
|
4
|
-
export * from './
|
|
1
|
+
export * from './application/model-controller'
|
|
2
|
+
export * from './application/read-only-model-controller'
|
|
3
|
+
export * from './domain/events/domain-event'
|
|
4
|
+
export * from './domain/events/event-types'
|
|
5
|
+
export * from './domain/events/mutation-event'
|
|
6
|
+
export * from './domain/events/query-event'
|
|
7
|
+
export * from './domain/events/request-event'
|
|
8
|
+
export * from './domain/interfaces/repository'
|
|
9
|
+
export * from './domain/models/pagination'
|
|
10
|
+
export * from './domain/services/base-model-service'
|
|
11
|
+
export * from './domain/services/model-service'
|
|
12
|
+
export * from './domain/services/model-service-args'
|
|
13
|
+
export * from './domain/services/read-only-model-service'
|
|
14
|
+
export * from './shared/utils/schema-inference'
|
|
15
|
+
export * from './shared/utils/schema-inheritance'
|
|
16
|
+
export * from './test/mock/models/mock-book-models'
|
|
17
|
+
export * from './test/mock/repositories/mock-memory-repository'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AnyModelSchema, InferModelInput, InferModelOutput } from '@declaro/core'
|
|
2
|
+
import type { IPagination } from '../../domain/models/pagination'
|
|
3
|
+
|
|
4
|
+
export interface ISearchResults<T> {
|
|
5
|
+
results: T[]
|
|
6
|
+
pagination: IPagination
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type InferLookup<TSchema extends AnyModelSchema> = InferModelInput<TSchema['definition']['lookup']>
|
|
10
|
+
export type InferDetail<TSchema extends AnyModelSchema> = InferModelOutput<TSchema['definition']['detail']>
|
|
11
|
+
export type InferFilters<TSchema extends AnyModelSchema> = InferModelInput<TSchema['definition']['filters']>
|
|
12
|
+
export type InferSummary<TSchema extends AnyModelSchema> = InferModelOutput<TSchema['definition']['summary']>
|
|
13
|
+
export type InferSort<TSchema extends AnyModelSchema> = InferModelInput<TSchema['definition']['sort']>
|
|
14
|
+
export type InferInput<TSchema extends AnyModelSchema> = InferModelInput<TSchema['definition']['input']>
|
|
15
|
+
export type InferSearchResults<TSchema extends AnyModelSchema> = ISearchResults<InferSummary<TSchema>>
|
|
16
|
+
export type InferEntityMetadata<TSchema extends AnyModelSchema> = ReturnType<TSchema['getEntityMetadata']>
|
|
17
|
+
export type InferPrimaryKeyType<TSchema extends AnyModelSchema> =
|
|
18
|
+
InferLookup<TSchema>[InferEntityMetadata<TSchema>['primaryKey']]
|
|
19
|
+
|
|
20
|
+
export type InferSummarySchema<TSchema extends AnyModelSchema> = TSchema['definition']['summary']['schema']
|
|
21
|
+
export type InferDetailSchema<TSchema extends AnyModelSchema> = TSchema['definition']['detail']['schema']
|
|
22
|
+
export type InferLookupSchema<TSchema extends AnyModelSchema> = TSchema['definition']['lookup']['schema']
|
|
23
|
+
export type InferInputSchema<TSchema extends AnyModelSchema> = TSchema['definition']['input']['schema']
|
|
24
|
+
export type InferFiltersSchema<TSchema extends AnyModelSchema> = TSchema['definition']['filters']['schema']
|
|
25
|
+
export type InferSortSchema<TSchema extends AnyModelSchema> = TSchema['definition']['sort']['schema']
|
|
26
|
+
export type InferSearchResultsSchema<TSchema extends AnyModelSchema> = TSchema['definition']['search']['schema']
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { ChildSchema } from './schema-inheritance'
|
|
3
|
+
import type { IAnimalTraitSchema } from './test/animal-trait-schema'
|
|
4
|
+
import type { IElephantSchema } from './test/elephant-schema'
|
|
5
|
+
import type { IAnimalSchema } from './test/animal-schema'
|
|
6
|
+
import type { IElephantTraitSchema } from './test/elephant-trait-schema'
|
|
7
|
+
import type { InferDetail } from './schema-inference'
|
|
8
|
+
|
|
9
|
+
// Service Classes using ChildSchema
|
|
10
|
+
|
|
11
|
+
class AnimalService<TSchema extends ChildSchema<IAnimalSchema>> {
|
|
12
|
+
getTraits(animal?: InferDetail<TSchema>): InferDetail<IAnimalTraitSchema>[] {
|
|
13
|
+
return animal?.traits ?? []
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class ElephantService extends AnimalService<IElephantSchema> {
|
|
18
|
+
getTrunkLength(elephant?: InferDetail<IElephantSchema>): number {
|
|
19
|
+
return elephant?.trunkLength ?? 0
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class AnimalTraitService<TSchema extends ChildSchema<IAnimalTraitSchema>> {
|
|
24
|
+
isTitleLong(trait?: InferDetail<TSchema>): boolean {
|
|
25
|
+
return (trait?.title.length ?? 0) > 10
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class ElephantTraitService extends AnimalTraitService<IElephantTraitSchema> {
|
|
30
|
+
isSharpTrait(trait?: InferDetail<IElephantTraitSchema>): boolean {
|
|
31
|
+
return trait?.isSharp ?? false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('Schema Inheritance Utils', () => {
|
|
36
|
+
describe('AnimalService', () => {
|
|
37
|
+
test('should get traits from animal detail', () => {
|
|
38
|
+
const service = new AnimalService()
|
|
39
|
+
const animal = {
|
|
40
|
+
name: 'Lion',
|
|
41
|
+
sound: 'Roar',
|
|
42
|
+
traits: [
|
|
43
|
+
{ title: 'Carnivore', description: 'Eats meat', animal: undefined },
|
|
44
|
+
{ title: 'Predator', description: 'Hunts prey', animal: undefined },
|
|
45
|
+
],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const traits = service.getTraits(animal)
|
|
49
|
+
|
|
50
|
+
expect(traits).toHaveLength(2)
|
|
51
|
+
expect(traits[0].title).toBe('Carnivore')
|
|
52
|
+
expect(traits[1].title).toBe('Predator')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('should return empty array when animal has no traits', () => {
|
|
56
|
+
const service = new AnimalService()
|
|
57
|
+
const animal = {
|
|
58
|
+
name: 'Lion',
|
|
59
|
+
sound: 'Roar',
|
|
60
|
+
traits: [],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const traits = service.getTraits(animal)
|
|
64
|
+
|
|
65
|
+
expect(traits).toEqual([])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('should return empty array when animal is undefined', () => {
|
|
69
|
+
const service = new AnimalService()
|
|
70
|
+
|
|
71
|
+
const traits = service.getTraits(undefined)
|
|
72
|
+
|
|
73
|
+
expect(traits).toEqual([])
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('ElephantService', () => {
|
|
78
|
+
test('should inherit getTraits from AnimalService', () => {
|
|
79
|
+
const service = new ElephantService()
|
|
80
|
+
const elephant = {
|
|
81
|
+
name: 'Dumbo',
|
|
82
|
+
sound: 'Trumpet',
|
|
83
|
+
trunkLength: 150,
|
|
84
|
+
traits: [
|
|
85
|
+
{
|
|
86
|
+
title: 'Long trunk',
|
|
87
|
+
description: 'Very long trunk',
|
|
88
|
+
isSharp: false,
|
|
89
|
+
length: 150,
|
|
90
|
+
animal: undefined,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const traits = service.getTraits(elephant)
|
|
96
|
+
|
|
97
|
+
expect(traits).toHaveLength(1)
|
|
98
|
+
expect(traits[0].title).toBe('Long trunk')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('should get trunk length from elephant', () => {
|
|
102
|
+
const service = new ElephantService()
|
|
103
|
+
const elephant = {
|
|
104
|
+
name: 'Dumbo',
|
|
105
|
+
sound: 'Trumpet',
|
|
106
|
+
trunkLength: 150,
|
|
107
|
+
traits: [],
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const trunkLength = service.getTrunkLength(elephant)
|
|
111
|
+
|
|
112
|
+
expect(trunkLength).toBe(150)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('should return 0 when elephant is undefined', () => {
|
|
116
|
+
const service = new ElephantService()
|
|
117
|
+
|
|
118
|
+
const trunkLength = service.getTrunkLength(undefined)
|
|
119
|
+
|
|
120
|
+
expect(trunkLength).toBe(0)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('should handle elephant with zero trunk length', () => {
|
|
124
|
+
const service = new ElephantService()
|
|
125
|
+
const elephant = {
|
|
126
|
+
name: 'Baby Elephant',
|
|
127
|
+
sound: 'Squeak',
|
|
128
|
+
trunkLength: 0,
|
|
129
|
+
traits: [],
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const trunkLength = service.getTrunkLength(elephant)
|
|
133
|
+
|
|
134
|
+
expect(trunkLength).toBe(0)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('AnimalTraitService', () => {
|
|
139
|
+
test('should identify long titles', () => {
|
|
140
|
+
const service = new AnimalTraitService()
|
|
141
|
+
const trait = {
|
|
142
|
+
title: 'This is a very long title',
|
|
143
|
+
description: 'Description',
|
|
144
|
+
animal: undefined,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const isLong = service.isTitleLong(trait)
|
|
148
|
+
|
|
149
|
+
expect(isLong).toBe(true)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('should identify short titles', () => {
|
|
153
|
+
const service = new AnimalTraitService()
|
|
154
|
+
const trait = {
|
|
155
|
+
title: 'Short',
|
|
156
|
+
description: 'Description',
|
|
157
|
+
animal: undefined,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const isLong = service.isTitleLong(trait)
|
|
161
|
+
|
|
162
|
+
expect(isLong).toBe(false)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('should handle exactly 10 character titles as short', () => {
|
|
166
|
+
const service = new AnimalTraitService()
|
|
167
|
+
const trait = {
|
|
168
|
+
title: '1234567890',
|
|
169
|
+
description: 'Description',
|
|
170
|
+
animal: undefined,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const isLong = service.isTitleLong(trait)
|
|
174
|
+
|
|
175
|
+
expect(isLong).toBe(false)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('should return false when trait is undefined', () => {
|
|
179
|
+
const service = new AnimalTraitService()
|
|
180
|
+
|
|
181
|
+
const isLong = service.isTitleLong(undefined)
|
|
182
|
+
|
|
183
|
+
expect(isLong).toBe(false)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('ElephantTraitService', () => {
|
|
188
|
+
test('should inherit isTitleLong from AnimalTraitService', () => {
|
|
189
|
+
const service = new ElephantTraitService()
|
|
190
|
+
const trait = {
|
|
191
|
+
title: 'Very long elephant trait title',
|
|
192
|
+
description: 'Description',
|
|
193
|
+
isSharp: true,
|
|
194
|
+
length: 50,
|
|
195
|
+
animal: undefined,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const isLong = service.isTitleLong(trait)
|
|
199
|
+
|
|
200
|
+
expect(isLong).toBe(true)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('should identify sharp traits', () => {
|
|
204
|
+
const service = new ElephantTraitService()
|
|
205
|
+
const trait = {
|
|
206
|
+
title: 'Sharp tusk',
|
|
207
|
+
description: 'A sharp tusk',
|
|
208
|
+
isSharp: true,
|
|
209
|
+
length: 100,
|
|
210
|
+
animal: undefined,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const isSharp = service.isSharpTrait(trait)
|
|
214
|
+
|
|
215
|
+
expect(isSharp).toBe(true)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('should identify non-sharp traits', () => {
|
|
219
|
+
const service = new ElephantTraitService()
|
|
220
|
+
const trait = {
|
|
221
|
+
title: 'Soft ear',
|
|
222
|
+
description: 'A soft ear',
|
|
223
|
+
isSharp: false,
|
|
224
|
+
length: 50,
|
|
225
|
+
animal: undefined,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const isSharp = service.isSharpTrait(trait)
|
|
229
|
+
|
|
230
|
+
expect(isSharp).toBe(false)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('should return false when trait is undefined', () => {
|
|
234
|
+
const service = new ElephantTraitService()
|
|
235
|
+
|
|
236
|
+
const isSharp = service.isSharpTrait(undefined)
|
|
237
|
+
|
|
238
|
+
expect(isSharp).toBe(false)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('should handle both inherited and own methods', () => {
|
|
242
|
+
const service = new ElephantTraitService()
|
|
243
|
+
const trait = {
|
|
244
|
+
title: 'Long and sharp tusk trait',
|
|
245
|
+
description: 'Description',
|
|
246
|
+
isSharp: true,
|
|
247
|
+
length: 150,
|
|
248
|
+
animal: undefined,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const isLong = service.isTitleLong(trait)
|
|
252
|
+
const isSharp = service.isSharpTrait(trait)
|
|
253
|
+
|
|
254
|
+
expect(isLong).toBe(true)
|
|
255
|
+
expect(isSharp).toBe(true)
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
describe('Schema Inheritance Type Safety', () => {
|
|
260
|
+
test('should allow ElephantService to work with elephant-specific properties', () => {
|
|
261
|
+
const service = new ElephantService()
|
|
262
|
+
const elephant = {
|
|
263
|
+
name: 'Babar',
|
|
264
|
+
sound: 'Trumpet',
|
|
265
|
+
trunkLength: 200,
|
|
266
|
+
traits: [],
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Should compile and work with both animal and elephant properties
|
|
270
|
+
const traits = service.getTraits(elephant)
|
|
271
|
+
const trunkLength = service.getTrunkLength(elephant)
|
|
272
|
+
|
|
273
|
+
expect(traits).toEqual([])
|
|
274
|
+
expect(trunkLength).toBe(200)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test('should allow ElephantTraitService to work with elephant trait properties', () => {
|
|
278
|
+
const service = new ElephantTraitService()
|
|
279
|
+
const trait = {
|
|
280
|
+
title: 'Trait',
|
|
281
|
+
description: 'Desc',
|
|
282
|
+
isSharp: true,
|
|
283
|
+
length: 75,
|
|
284
|
+
animal: undefined,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Should compile and work with both animal trait and elephant trait properties
|
|
288
|
+
const isLong = service.isTitleLong(trait)
|
|
289
|
+
const isSharp = service.isSharpTrait(trait)
|
|
290
|
+
|
|
291
|
+
expect(isLong).toBe(false)
|
|
292
|
+
expect(isSharp).toBe(true)
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AnyModelSchema, Model, ModelSchema } from '@declaro/core'
|
|
2
|
+
import type { InferEntityMetadata } from './schema-inference'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Represents a child schema that inherits from a parent schema.
|
|
6
|
+
* This is useful for creating schemas that extend the functionality of an existing schema.
|
|
7
|
+
* It replaces the schema name and all model names with a string type.
|
|
8
|
+
*
|
|
9
|
+
* @warning This type is intended for use in generic types. In most cases, you should use concrete schemas for inheritance for best type inference.
|
|
10
|
+
*
|
|
11
|
+
* @template TSchema - The parent schema type.
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { ModelSchema, ChildSchema } from '@declaro/core';
|
|
15
|
+
*
|
|
16
|
+
* export class ParentService<TSchema extends ChildSchema<typeof ParentSchema>> extends ModelService<TSchema> {
|
|
17
|
+
* constructor(args: IModelServiceArgs<TSchema>) {
|
|
18
|
+
* super(args);
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
export type ChildSchema<TSchema extends AnyModelSchema> = ModelSchema<
|
|
23
|
+
string,
|
|
24
|
+
{
|
|
25
|
+
[K in keyof TSchema['definition']]: Model<string, TSchema['definition'][K]['schema']>
|
|
26
|
+
},
|
|
27
|
+
InferEntityMetadata<TSchema>
|
|
28
|
+
>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Animal Schema
|
|
2
|
+
|
|
3
|
+
import { ModelSchema } from '@declaro/core'
|
|
4
|
+
import { sortArray, ZodModel } from '@declaro/zod'
|
|
5
|
+
import z4 from 'zod/v4'
|
|
6
|
+
import { AnimalTraitsDetailSchema, AnimalTraitsSummarySchema } from './animal-trait-schema'
|
|
7
|
+
|
|
8
|
+
export const AnimalDetailSchema = z4.object({
|
|
9
|
+
name: z4.string(),
|
|
10
|
+
sound: z4.string(),
|
|
11
|
+
get traits() {
|
|
12
|
+
return z4.array(AnimalTraitsDetailSchema)
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const AnimalLookupSchema = z4.object({
|
|
17
|
+
id: z4.number(),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export const AnimalFiltersSchema = z4.object({
|
|
21
|
+
text: z4.string().optional(),
|
|
22
|
+
sound: z4.string().optional(),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export const AnimalSortSchema = sortArray(['name', 'sound'])
|
|
26
|
+
|
|
27
|
+
export const AnimalSummarySchema = z4.object({
|
|
28
|
+
name: z4.string(),
|
|
29
|
+
sound: z4.string(),
|
|
30
|
+
get traits() {
|
|
31
|
+
return z4.array(AnimalTraitsSummarySchema)
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export const AnimalSchema = ModelSchema.create('AnimalSchema')
|
|
36
|
+
.read({
|
|
37
|
+
detail: (h) => new ZodModel('AnimalDetail', AnimalDetailSchema),
|
|
38
|
+
lookup: (h) => new ZodModel('AnimalLookup', AnimalLookupSchema),
|
|
39
|
+
})
|
|
40
|
+
.search({
|
|
41
|
+
filters: (h) => new ZodModel('AnimalFilters', AnimalFiltersSchema),
|
|
42
|
+
sort: (h) => new ZodModel('AnimalSort', AnimalSortSchema),
|
|
43
|
+
summary: (h) => new ZodModel('AnimalSummary', AnimalSummarySchema),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
export type IAnimalSchema = typeof AnimalSchema
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { sortArray, ZodModel } from '@declaro/zod'
|
|
2
|
+
import { AnimalDetailSchema, AnimalSummarySchema } from './animal-schema'
|
|
3
|
+
|
|
4
|
+
// Animal Traits Schema
|
|
5
|
+
|
|
6
|
+
import { ModelSchema } from '@declaro/core'
|
|
7
|
+
import z4 from 'zod/v4'
|
|
8
|
+
|
|
9
|
+
export const AnimalTraitsDetailSchema = z4.object({
|
|
10
|
+
title: z4.string(),
|
|
11
|
+
description: z4.string(),
|
|
12
|
+
get animal() {
|
|
13
|
+
return AnimalDetailSchema.optional()
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const AnimalTraitsSummarySchema = z4.object({
|
|
18
|
+
title: z4.string(),
|
|
19
|
+
get animal() {
|
|
20
|
+
return AnimalSummarySchema.optional()
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const AnimalTraitsLookupSchema = z4.object({
|
|
25
|
+
id: z4.number(),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const AnimalTraitsFiltersSchema = z4.object({
|
|
29
|
+
text: z4.string().optional(),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export const AnimalTraitsSortSchema = sortArray(['title'])
|
|
33
|
+
|
|
34
|
+
export const AnimalTraitsSchema = ModelSchema.create('AnimalTraitsSchema')
|
|
35
|
+
.read({
|
|
36
|
+
detail: (h) => new ZodModel('AnimalTraitsDetail', AnimalTraitsDetailSchema),
|
|
37
|
+
lookup: (h) => new ZodModel('AnimalTraitsLookup', AnimalTraitsLookupSchema),
|
|
38
|
+
})
|
|
39
|
+
.search({
|
|
40
|
+
filters: (h) => new ZodModel('AnimalTraitsFilters', AnimalTraitsFiltersSchema),
|
|
41
|
+
sort: (h) => new ZodModel('AnimalTraitsSort', AnimalTraitsSortSchema),
|
|
42
|
+
summary: (h) => new ZodModel('AnimalTraitsSummary', AnimalTraitsSummarySchema),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export type IAnimalTraitSchema = typeof AnimalTraitsSchema
|