@declaro/data 2.0.0-beta.12 → 2.0.0-beta.121

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.
Files changed (130) hide show
  1. package/{LICENSE → LICENSE.md} +1 -1
  2. package/README.md +0 -0
  3. package/dist/browser/index.js +26 -0
  4. package/dist/browser/index.js.map +93 -0
  5. package/dist/node/index.cjs +13226 -0
  6. package/dist/node/index.cjs.map +93 -0
  7. package/dist/node/index.js +13205 -0
  8. package/dist/node/index.js.map +93 -0
  9. package/dist/ts/application/model-controller.d.ts +50 -0
  10. package/dist/ts/application/model-controller.d.ts.map +1 -0
  11. package/dist/ts/application/model-controller.test.d.ts +2 -0
  12. package/dist/ts/application/model-controller.test.d.ts.map +1 -0
  13. package/dist/ts/application/read-only-model-controller.d.ts +20 -0
  14. package/dist/ts/application/read-only-model-controller.d.ts.map +1 -0
  15. package/dist/ts/application/read-only-model-controller.test.d.ts +2 -0
  16. package/dist/ts/application/read-only-model-controller.test.d.ts.map +1 -0
  17. package/dist/ts/domain/events/domain-event.d.ts +41 -0
  18. package/dist/ts/domain/events/domain-event.d.ts.map +1 -0
  19. package/dist/ts/domain/events/domain-event.test.d.ts +2 -0
  20. package/dist/ts/domain/events/domain-event.test.d.ts.map +1 -0
  21. package/dist/ts/domain/events/event-types.d.ts +34 -0
  22. package/dist/ts/domain/events/event-types.d.ts.map +1 -0
  23. package/dist/ts/domain/events/mutation-event.d.ts +6 -0
  24. package/dist/ts/domain/events/mutation-event.d.ts.map +1 -0
  25. package/dist/ts/domain/events/mutation-event.test.d.ts +2 -0
  26. package/dist/ts/domain/events/mutation-event.test.d.ts.map +1 -0
  27. package/dist/ts/domain/events/query-event.d.ts +6 -0
  28. package/dist/ts/domain/events/query-event.d.ts.map +1 -0
  29. package/dist/ts/domain/events/query-event.test.d.ts +2 -0
  30. package/dist/ts/domain/events/query-event.test.d.ts.map +1 -0
  31. package/dist/ts/domain/events/request-event.d.ts +11 -0
  32. package/dist/ts/domain/events/request-event.d.ts.map +1 -0
  33. package/dist/ts/domain/events/request-event.test.d.ts +2 -0
  34. package/dist/ts/domain/events/request-event.test.d.ts.map +1 -0
  35. package/dist/ts/domain/interfaces/repository.d.ts +110 -0
  36. package/dist/ts/domain/interfaces/repository.d.ts.map +1 -0
  37. package/dist/ts/domain/models/pagination.d.ts +28 -0
  38. package/dist/ts/domain/models/pagination.d.ts.map +1 -0
  39. package/dist/ts/domain/services/base-model-service.d.ts +23 -0
  40. package/dist/ts/domain/services/base-model-service.d.ts.map +1 -0
  41. package/dist/ts/domain/services/model-service-args.d.ts +9 -0
  42. package/dist/ts/domain/services/model-service-args.d.ts.map +1 -0
  43. package/dist/ts/domain/services/model-service.d.ts +72 -0
  44. package/dist/ts/domain/services/model-service.d.ts.map +1 -0
  45. package/dist/ts/domain/services/model-service.normalization.test.d.ts +2 -0
  46. package/dist/ts/domain/services/model-service.normalization.test.d.ts.map +1 -0
  47. package/dist/ts/domain/services/model-service.test.d.ts +2 -0
  48. package/dist/ts/domain/services/model-service.test.d.ts.map +1 -0
  49. package/dist/ts/domain/services/read-only-model-service.d.ts +76 -0
  50. package/dist/ts/domain/services/read-only-model-service.d.ts.map +1 -0
  51. package/dist/ts/domain/services/read-only-model-service.test.d.ts +2 -0
  52. package/dist/ts/domain/services/read-only-model-service.test.d.ts.map +1 -0
  53. package/dist/ts/index.d.ts +18 -0
  54. package/dist/ts/index.d.ts.map +1 -0
  55. package/dist/ts/shared/utils/schema-inference.d.ts +23 -0
  56. package/dist/ts/shared/utils/schema-inference.d.ts.map +1 -0
  57. package/dist/ts/shared/utils/schema-inheritance.d.ts +24 -0
  58. package/dist/ts/shared/utils/schema-inheritance.d.ts.map +1 -0
  59. package/dist/ts/test/mock/models/mock-book-models.d.ts +42 -0
  60. package/dist/ts/test/mock/models/mock-book-models.d.ts.map +1 -0
  61. package/dist/ts/test/mock/repositories/mock-memory-repository.assign.test.d.ts +2 -0
  62. package/dist/ts/test/mock/repositories/mock-memory-repository.assign.test.d.ts.map +1 -0
  63. package/dist/ts/test/mock/repositories/mock-memory-repository.basic.test.d.ts +2 -0
  64. package/dist/ts/test/mock/repositories/mock-memory-repository.basic.test.d.ts.map +1 -0
  65. package/dist/ts/test/mock/repositories/mock-memory-repository.bulk-upsert.test.d.ts +2 -0
  66. package/dist/ts/test/mock/repositories/mock-memory-repository.bulk-upsert.test.d.ts.map +1 -0
  67. package/dist/ts/test/mock/repositories/mock-memory-repository.count.test.d.ts +2 -0
  68. package/dist/ts/test/mock/repositories/mock-memory-repository.count.test.d.ts.map +1 -0
  69. package/dist/ts/test/mock/repositories/mock-memory-repository.custom-lookup.test.d.ts +1 -0
  70. package/dist/ts/test/mock/repositories/mock-memory-repository.custom-lookup.test.d.ts.map +1 -0
  71. package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts +62 -0
  72. package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts.map +1 -0
  73. package/dist/ts/test/mock/repositories/mock-memory-repository.search.test.d.ts +2 -0
  74. package/dist/ts/test/mock/repositories/mock-memory-repository.search.test.d.ts.map +1 -0
  75. package/dist/ts/test/mock/repositories/mock-memory-repository.trash.test.d.ts +2 -0
  76. package/dist/ts/test/mock/repositories/mock-memory-repository.trash.test.d.ts.map +1 -0
  77. package/dist/ts/test/mock/repositories/mock-memory-repository.upsert.test.d.ts +2 -0
  78. package/dist/ts/test/mock/repositories/mock-memory-repository.upsert.test.d.ts.map +1 -0
  79. package/package.json +45 -42
  80. package/src/application/model-controller.test.ts +694 -0
  81. package/src/application/model-controller.ts +135 -0
  82. package/src/application/read-only-model-controller.test.ts +335 -0
  83. package/src/application/read-only-model-controller.ts +61 -0
  84. package/src/domain/events/domain-event.test.ts +82 -0
  85. package/src/domain/events/domain-event.ts +69 -0
  86. package/src/domain/events/event-types.ts +34 -0
  87. package/src/domain/events/mutation-event.test.ts +38 -0
  88. package/src/domain/events/mutation-event.ts +8 -0
  89. package/src/domain/events/query-event.test.ts +28 -0
  90. package/src/domain/events/query-event.ts +8 -0
  91. package/src/domain/events/request-event.test.ts +38 -0
  92. package/src/domain/events/request-event.ts +32 -0
  93. package/src/domain/interfaces/repository.ts +136 -0
  94. package/src/domain/models/pagination.ts +28 -0
  95. package/src/domain/services/base-model-service.ts +54 -0
  96. package/src/domain/services/model-service-args.ts +9 -0
  97. package/src/domain/services/model-service.normalization.test.ts +704 -0
  98. package/src/domain/services/model-service.test.ts +940 -0
  99. package/src/domain/services/model-service.ts +432 -0
  100. package/src/domain/services/read-only-model-service.test.ts +828 -0
  101. package/src/domain/services/read-only-model-service.ts +178 -0
  102. package/src/index.ts +17 -4
  103. package/src/shared/utils/schema-inference.ts +26 -0
  104. package/src/shared/utils/schema-inheritance.ts +28 -0
  105. package/src/test/mock/models/mock-book-models.ts +78 -0
  106. package/src/test/mock/repositories/mock-memory-repository.assign.test.ts +215 -0
  107. package/src/test/mock/repositories/mock-memory-repository.basic.test.ts +129 -0
  108. package/src/test/mock/repositories/mock-memory-repository.bulk-upsert.test.ts +159 -0
  109. package/src/test/mock/repositories/mock-memory-repository.count.test.ts +98 -0
  110. package/src/test/mock/repositories/mock-memory-repository.custom-lookup.test.ts +0 -0
  111. package/src/test/mock/repositories/mock-memory-repository.search.test.ts +265 -0
  112. package/src/test/mock/repositories/mock-memory-repository.trash.test.ts +736 -0
  113. package/src/test/mock/repositories/mock-memory-repository.ts +401 -0
  114. package/src/test/mock/repositories/mock-memory-repository.upsert.test.ts +108 -0
  115. package/dist/databaseConnection.d.ts +0 -24
  116. package/dist/datastoreAbstract.d.ts +0 -37
  117. package/dist/declaro-data.cjs +0 -1
  118. package/dist/declaro-data.mjs +0 -250
  119. package/dist/hydrateEntity.d.ts +0 -8
  120. package/dist/index.d.ts +0 -4
  121. package/dist/serverConnection.d.ts +0 -15
  122. package/dist/trackedStatus.d.ts +0 -15
  123. package/src/databaseConnection.ts +0 -137
  124. package/src/datastoreAbstract.ts +0 -190
  125. package/src/hydrateEntity.ts +0 -36
  126. package/src/placeholder.test.ts +0 -7
  127. package/src/serverConnection.ts +0 -74
  128. package/src/trackedStatus.ts +0 -35
  129. package/tsconfig.json +0 -10
  130. package/vite.config.ts +0 -23
@@ -0,0 +1,178 @@
1
+ import type { AnyModelSchema, Model } from '@declaro/core'
2
+ import type {
3
+ InferDetail,
4
+ InferFilters,
5
+ InferLookup,
6
+ InferSearchResults,
7
+ InferSort,
8
+ } from '../../shared/utils/schema-inference'
9
+ import { ModelQueryEvent } from '../events/event-types'
10
+ import { QueryEvent } from '../events/query-event'
11
+ import { BaseModelService, type IActionOptions } from './base-model-service'
12
+ import type { IPaginationInput } from '../models/pagination'
13
+
14
+ /**
15
+ * Options for loading records.
16
+ */
17
+ export interface ILoadOptions extends IActionOptions {
18
+ /**
19
+ * If true, only removed (soft-deleted) records will be returned.
20
+ */
21
+ removedOnly?: boolean
22
+ /**
23
+ * If true, both removed and non-removed records will be returned.
24
+ */
25
+ includeRemoved?: boolean
26
+ }
27
+ export interface ISearchOptions<TSchema extends AnyModelSchema> extends IActionOptions {
28
+ pagination?: IPaginationInput
29
+ sort?: InferSort<TSchema>
30
+ /**
31
+ * If true, only removed (soft-deleted) records will be returned.
32
+ */
33
+ removedOnly?: boolean
34
+ /**
35
+ * If true, both removed and non-removed records will be returned.
36
+ */
37
+ includeRemoved?: boolean
38
+ }
39
+
40
+ export class ReadOnlyModelService<TSchema extends AnyModelSchema> extends BaseModelService<TSchema> {
41
+ /**
42
+ * Normalize the detail data to match the expected schema.
43
+ * WARNING: This method is called once per detail in load operations.
44
+ * Any intensive operations or queries should be avoided here, and done via bulk operations in the respective methods such as `loadMany` instead.
45
+ * @param detail The detail data to normalize.
46
+ * @returns The normalized detail data.
47
+ */
48
+ async normalizeDetail(detail: InferDetail<TSchema>): Promise<InferDetail<TSchema>> {
49
+ return detail
50
+ }
51
+
52
+ /**
53
+ * Normalize the summary data to match the expected schema.
54
+ * WARNING: This method is called once per summary in search results, often in parallel.
55
+ * Any intensive operations or queries should be avoided here, and done via bulk operations in the respective methods such as `search` instead.
56
+ *
57
+ * @param summary The summary data to normalize.
58
+ * @returns The normalized summary data.
59
+ */
60
+ async normalizeSummary(summary: InferDetail<TSchema>): Promise<InferDetail<TSchema>> {
61
+ return summary
62
+ }
63
+
64
+ /**
65
+ * Load a single record by its lookup criteria.
66
+ * @param lookup The lookup criteria to find the record.
67
+ * @param options Additional options for the load operation.
68
+ * @returns The loaded record details.
69
+ */
70
+ async load(lookup: InferLookup<TSchema>, options?: ILoadOptions): Promise<InferDetail<TSchema>> {
71
+ // Emit the before load event
72
+ const beforeLoadEvent = new QueryEvent<InferDetail<TSchema>, InferLookup<TSchema>>(
73
+ this.getDescriptor(ModelQueryEvent.BeforeLoad, options?.scope),
74
+ lookup,
75
+ )
76
+ await this.emitter.emitAsync(beforeLoadEvent)
77
+
78
+ // Load the details from the repository
79
+ const details = await this.repository.load(lookup, options)
80
+
81
+ // Emit the after load event
82
+ const afterLoadEvent = new QueryEvent<InferDetail<TSchema>, InferLookup<TSchema>>(
83
+ this.getDescriptor(ModelQueryEvent.AfterLoad, options?.scope),
84
+ lookup,
85
+ ).setResult(details)
86
+ await this.emitter.emitAsync(afterLoadEvent)
87
+
88
+ return await this.normalizeDetail(details)
89
+ }
90
+
91
+ /**
92
+ * Load multiple records by their lookup criteria.
93
+ * @param lookups The lookup criteria to find the records.
94
+ * @param options Additional options for the load operation.
95
+ * @returns An array of loaded record details.
96
+ */
97
+ async loadMany(lookups: InferLookup<TSchema>[], options?: ILoadOptions): Promise<InferDetail<TSchema>[]> {
98
+ // Emit the before load many event
99
+ const beforeLoadManyEvent = new QueryEvent<InferDetail<TSchema>[], InferLookup<TSchema>[]>(
100
+ this.getDescriptor(ModelQueryEvent.BeforeLoadMany, options?.scope),
101
+ lookups,
102
+ )
103
+ await this.emitter.emitAsync(beforeLoadManyEvent)
104
+
105
+ // Load the details from the repository
106
+ const details = await this.repository.loadMany(lookups, options)
107
+
108
+ // Emit the after load many event
109
+ const afterLoadManyEvent = new QueryEvent<InferDetail<TSchema>[], InferLookup<TSchema>[]>(
110
+ this.getDescriptor(ModelQueryEvent.AfterLoadMany, options?.scope),
111
+ lookups,
112
+ ).setResult(details)
113
+ await this.emitter.emitAsync(afterLoadManyEvent)
114
+
115
+ return await Promise.all(details.map((detail) => this.normalizeDetail(detail)))
116
+ }
117
+
118
+ /**
119
+ * Search for records matching the given filters.
120
+ * @param filters The filters to apply to the search.
121
+ * @param options Additional options for the search operation.
122
+ * @returns The search results.
123
+ */
124
+ async search(
125
+ filters: InferFilters<TSchema>,
126
+ options?: ISearchOptions<TSchema>,
127
+ ): Promise<InferSearchResults<TSchema>> {
128
+ // Emit the before search event
129
+ const beforeSearchEvent = new QueryEvent<InferSearchResults<TSchema>, InferFilters<TSchema>>(
130
+ this.getDescriptor(ModelQueryEvent.BeforeSearch, options?.scope),
131
+ filters,
132
+ )
133
+ await this.emitter.emitAsync(beforeSearchEvent)
134
+
135
+ // Search the repository with the provided filters
136
+ const results = await this.repository.search(filters, options)
137
+
138
+ // Emit the after search event
139
+ const afterSearchEvent = new QueryEvent<InferSearchResults<TSchema>, InferFilters<TSchema>>(
140
+ this.getDescriptor(ModelQueryEvent.AfterSearch, options?.scope),
141
+ filters,
142
+ ).setResult(results)
143
+ await this.emitter.emitAsync(afterSearchEvent)
144
+
145
+ // Return the search results
146
+ return {
147
+ ...results,
148
+ results: await Promise.all(results.results.map((detail) => this.normalizeSummary(detail))),
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Count the number of records matching the given filters.
154
+ * @param filters The filters to apply to the count operation.
155
+ * @returns The count of matching records.
156
+ */
157
+ async count(filters: InferFilters<TSchema>, options?: ISearchOptions<TSchema>): Promise<number> {
158
+ // Emit the before count event
159
+ const beforeCountEvent = new QueryEvent<number, InferFilters<TSchema>>(
160
+ this.getDescriptor(ModelQueryEvent.BeforeCount, options?.scope),
161
+ filters,
162
+ )
163
+ await this.emitter.emitAsync(beforeCountEvent)
164
+
165
+ // Count the records in the repository
166
+ const count = await this.repository.count(filters, options)
167
+
168
+ // Emit the after count event
169
+ const afterCountEvent = new QueryEvent<number, InferFilters<TSchema>>(
170
+ this.getDescriptor(ModelQueryEvent.AfterCount, options?.scope),
171
+ filters,
172
+ ).setResult(count)
173
+ await this.emitter.emitAsync(afterCountEvent)
174
+
175
+ // Return the count
176
+ return count
177
+ }
178
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,17 @@
1
- export * from './databaseConnection'
2
- export * from './serverConnection'
3
- export * from './datastoreAbstract'
4
- export * from './hydrateEntity'
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,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,78 @@
1
+ import { ModelSchema } from '@declaro/core'
2
+ import { sortArray, ZodModel } from '@declaro/zod'
3
+ import { z } from 'zod/v4'
4
+ import type {
5
+ InferFilters,
6
+ InferLookup,
7
+ InferDetail,
8
+ InferSummary,
9
+ InferSort,
10
+ InferInput,
11
+ InferSearchResults,
12
+ InferEntityMetadata,
13
+ } from '../../../shared/utils/schema-inference'
14
+
15
+ export const MockBookSchema = ModelSchema.create('Book')
16
+ .read({
17
+ detail: (h) =>
18
+ new ZodModel(
19
+ h.name,
20
+ z.object({
21
+ id: z.number().int().positive(),
22
+ title: z.string().min(2).max(100),
23
+ author: z.string().min(2).max(100),
24
+ publishedDate: z.coerce.date(),
25
+ }),
26
+ ),
27
+ lookup: (h) =>
28
+ new ZodModel(
29
+ h.name,
30
+ z.object({
31
+ id: z.number().int().positive(),
32
+ }),
33
+ ),
34
+ })
35
+ .search({
36
+ filters: (h) =>
37
+ new ZodModel(
38
+ h.name,
39
+ z.object({
40
+ text: z.string().optional(),
41
+ }),
42
+ ),
43
+ summary: (h) =>
44
+ new ZodModel(
45
+ h.name,
46
+ z.object({
47
+ id: z.number().int().positive(),
48
+ title: z.string().min(2).max(100),
49
+ author: z.string().min(2).max(100),
50
+ publishedDate: z.coerce.date(),
51
+ }),
52
+ ),
53
+ sort: (h) => new ZodModel(h.name, sortArray(['title', 'author'])),
54
+ })
55
+ .write({
56
+ input: (h) =>
57
+ new ZodModel(
58
+ h.name,
59
+ z.object({
60
+ id: z.number().int().positive().optional(),
61
+ title: z.string().min(2).max(100),
62
+ author: z.string().min(2).max(100),
63
+ publishedDate: z.coerce.date(),
64
+ }),
65
+ ),
66
+ })
67
+ .entity({
68
+ primaryKey: 'id',
69
+ })
70
+
71
+ export type MockBookFilters = InferFilters<typeof MockBookSchema>
72
+ export type MockBookLookup = InferLookup<typeof MockBookSchema>
73
+ export type MockBookDetail = InferDetail<typeof MockBookSchema>
74
+ export type MockBookSummary = InferSummary<typeof MockBookSchema>
75
+ export type MockBookSort = InferSort<typeof MockBookSchema>
76
+ export type MockBookInput = InferInput<typeof MockBookSchema>
77
+ export type MockBookSearchResults = InferSearchResults<typeof MockBookSchema>
78
+ export type MockBookEntityMetadata = InferEntityMetadata<typeof MockBookSchema>
@@ -0,0 +1,215 @@
1
+ import { beforeEach, describe, expect, it, mock } from 'bun:test'
2
+ import { MockBookSchema } from '../models/mock-book-models'
3
+ import { MockMemoryRepository } from './mock-memory-repository'
4
+
5
+ describe('MockMemoryRepository - Assign Functionality', () => {
6
+ const mockSchema = MockBookSchema
7
+
8
+ let repository: MockMemoryRepository<typeof mockSchema>
9
+
10
+ beforeEach(() => {
11
+ repository = new MockMemoryRepository({ schema: mockSchema })
12
+ })
13
+
14
+ it('should use default Object.assign for create when no custom assign function is provided', async () => {
15
+ const input = { title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
16
+ const createdItem = await repository.create(input)
17
+
18
+ expect(createdItem).toMatchObject(input)
19
+ expect(createdItem.id).toBeDefined()
20
+ })
21
+
22
+ it('should use default Object.assign for update when no custom assign function is provided', async () => {
23
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
24
+ const createdItem = await repository.create(input)
25
+
26
+ const updateInput = { title: 'Updated Book', author: 'Updated Author', publishedDate: new Date() }
27
+ const updatedItem = await repository.update({ id: createdItem.id }, updateInput)
28
+
29
+ expect(updatedItem).toEqual({
30
+ id: createdItem.id,
31
+ title: 'Updated Book',
32
+ author: 'Updated Author',
33
+ publishedDate: updateInput.publishedDate,
34
+ })
35
+ })
36
+
37
+ it('should use default Object.assign for upsert when no custom assign function is provided', async () => {
38
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
39
+ const upsertedItem = await repository.upsert(input)
40
+
41
+ expect(upsertedItem).toEqual(input)
42
+
43
+ const updateInput = { id: 42, title: 'Updated Book', author: 'Updated Author', publishedDate: new Date() }
44
+ const updatedItem = await repository.upsert(updateInput)
45
+
46
+ expect(updatedItem).toEqual(updateInput)
47
+ })
48
+
49
+ it('should use custom assign function for create operation', async () => {
50
+ const customAssignMock = mock((existing: any, input: any) => ({
51
+ ...existing,
52
+ ...input,
53
+ customField: 'custom_create_value',
54
+ }))
55
+
56
+ const customRepository = new MockMemoryRepository({
57
+ schema: mockSchema,
58
+ assign: customAssignMock,
59
+ })
60
+
61
+ const input = { title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
62
+ const createdItem = await customRepository.create(input)
63
+
64
+ expect(customAssignMock).toHaveBeenCalledWith({}, input)
65
+ expect((createdItem as any).customField).toBe('custom_create_value')
66
+ expect(createdItem).toMatchObject(input)
67
+ })
68
+
69
+ it('should use custom assign function for update operation', async () => {
70
+ const customAssignMock = mock((existing: any, input: any) => ({
71
+ ...existing,
72
+ ...input,
73
+ customField: 'custom_update_value',
74
+ lastModified: new Date('2023-01-01'),
75
+ }))
76
+
77
+ const customRepository = new MockMemoryRepository({
78
+ schema: mockSchema,
79
+ assign: customAssignMock,
80
+ })
81
+
82
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
83
+ const createdItem = await customRepository.create(input)
84
+
85
+ const updateInput = { title: 'Updated Book', author: 'Updated Author', publishedDate: new Date() }
86
+ const updatedItem = await customRepository.update({ id: createdItem.id }, updateInput)
87
+
88
+ expect(customAssignMock).toHaveBeenCalledWith(createdItem, updateInput)
89
+ expect((updatedItem as any).customField).toBe('custom_update_value')
90
+ expect((updatedItem as any).lastModified).toEqual(new Date('2023-01-01'))
91
+ expect(updatedItem.title).toBe('Updated Book')
92
+ })
93
+
94
+ it('should use custom assign function for upsert operation on existing item', async () => {
95
+ const customAssignMock = mock((existing: any, input: any) => ({
96
+ ...existing,
97
+ ...input,
98
+ mergeTimestamp: new Date('2023-01-01'),
99
+ isUpserted: true,
100
+ }))
101
+
102
+ const customRepository = new MockMemoryRepository({
103
+ schema: mockSchema,
104
+ assign: customAssignMock,
105
+ })
106
+
107
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
108
+ const createdItem = await customRepository.create(input)
109
+
110
+ const upsertInput = { id: 42, title: 'Upserted Book', author: 'Upserted Author', publishedDate: new Date() }
111
+ const upsertedItem = await customRepository.upsert(upsertInput)
112
+
113
+ // Should have been called twice: once for create, once for upsert
114
+ expect(customAssignMock).toHaveBeenCalledTimes(2)
115
+ expect(customAssignMock).toHaveBeenLastCalledWith(createdItem, upsertInput)
116
+ expect((upsertedItem as any).mergeTimestamp).toEqual(new Date('2023-01-01'))
117
+ expect((upsertedItem as any).isUpserted).toBe(true)
118
+ expect(upsertedItem.title).toBe('Upserted Book')
119
+ })
120
+
121
+ it('should use custom assign function for upsert operation on new item', async () => {
122
+ const customAssignMock = mock((existing: any, input: any) => ({
123
+ ...existing,
124
+ ...input,
125
+ createdViaUpsert: true,
126
+ }))
127
+
128
+ const customRepository = new MockMemoryRepository({
129
+ schema: mockSchema,
130
+ assign: customAssignMock,
131
+ })
132
+
133
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
134
+ const upsertedItem = await customRepository.upsert(input)
135
+
136
+ expect(customAssignMock).toHaveBeenCalledWith({}, input)
137
+ expect((upsertedItem as any).createdViaUpsert).toBe(true)
138
+ expect(upsertedItem).toMatchObject(input)
139
+ })
140
+
141
+ it('should use custom assign function for bulkUpsert operation', async () => {
142
+ const customAssignMock = mock((existing: any, input: any) => ({
143
+ ...existing,
144
+ ...input,
145
+ bulkProcessed: true,
146
+ }))
147
+
148
+ const customRepository = new MockMemoryRepository({
149
+ schema: mockSchema,
150
+ assign: customAssignMock,
151
+ })
152
+
153
+ const inputs = [
154
+ { id: 1, title: 'Book 1', author: 'Author 1', publishedDate: new Date() },
155
+ { id: 2, title: 'Book 2', author: 'Author 2', publishedDate: new Date() },
156
+ ]
157
+
158
+ const results = await customRepository.bulkUpsert(inputs)
159
+
160
+ expect(customAssignMock).toHaveBeenCalledTimes(2)
161
+ expect((results[0] as any).bulkProcessed).toBe(true)
162
+ expect((results[1] as any).bulkProcessed).toBe(true)
163
+ expect(results[0]).toMatchObject(inputs[0])
164
+ expect(results[1]).toMatchObject(inputs[1])
165
+ })
166
+
167
+ it('should handle complex custom assign logic with conditional merging', async () => {
168
+ const customAssign = (existing: any, input: any) => {
169
+ const result = { ...existing, ...input }
170
+
171
+ // Custom logic: preserve original author if input doesn't have one
172
+ if (!input.author && existing.author) {
173
+ result.author = existing.author
174
+ }
175
+
176
+ // Custom logic: track modification count
177
+ result.modificationCount = (existing.modificationCount || 0) + 1
178
+
179
+ return result
180
+ }
181
+
182
+ const customRepository = new MockMemoryRepository({
183
+ schema: mockSchema,
184
+ assign: customAssign,
185
+ })
186
+
187
+ const input = { id: 42, title: 'Test Book', author: 'Original Author', publishedDate: new Date() }
188
+ const createdItem = await customRepository.create(input)
189
+
190
+ expect((createdItem as any).modificationCount).toBe(1)
191
+
192
+ // Update with partial data - use existing values for required fields
193
+ const updateInput = {
194
+ title: 'Updated Book',
195
+ author: createdItem.author,
196
+ publishedDate: createdItem.publishedDate,
197
+ }
198
+ const updatedItem = await customRepository.update({ id: createdItem.id }, updateInput)
199
+
200
+ expect(updatedItem.author).toBe('Original Author')
201
+ expect(updatedItem.title).toBe('Updated Book')
202
+ expect((updatedItem as any).modificationCount).toBe(2)
203
+
204
+ // Update with new author
205
+ const updateWithAuthor = {
206
+ title: updatedItem.title,
207
+ author: 'New Author',
208
+ publishedDate: updatedItem.publishedDate,
209
+ }
210
+ const finalItem = await customRepository.update({ id: createdItem.id }, updateWithAuthor)
211
+
212
+ expect(finalItem.author).toBe('New Author')
213
+ expect((finalItem as any).modificationCount).toBe(3)
214
+ })
215
+ })
@@ -0,0 +1,129 @@
1
+ import { beforeEach, describe, expect, it } from 'bun:test'
2
+ import { MockBookSchema } from '../models/mock-book-models'
3
+ import { MockMemoryRepository } from './mock-memory-repository'
4
+ import { z } from 'zod/v4'
5
+ import { ZodModel } from '@declaro/zod'
6
+
7
+ describe('MockMemoryRepository - Basic Operations', () => {
8
+ const mockSchema = MockBookSchema
9
+
10
+ let repository: MockMemoryRepository<typeof mockSchema>
11
+
12
+ beforeEach(() => {
13
+ repository = new MockMemoryRepository({ schema: mockSchema })
14
+ })
15
+
16
+ it('should create an item', async () => {
17
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
18
+ const createdItem = await repository.create(input)
19
+
20
+ expect(createdItem).toEqual(input)
21
+ expect(await repository.load({ id: createdItem.id })).toEqual(createdItem)
22
+ })
23
+
24
+ it('should throw an error when creating an item with duplicate primary key', async () => {
25
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
26
+ const createdItem = await repository.create(input)
27
+
28
+ await expect(repository.create({ ...input, id: createdItem.id })).rejects.toThrow(
29
+ 'Item with the same primary key already exists',
30
+ )
31
+ })
32
+
33
+ it('should update an existing item', async () => {
34
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
35
+ const createdItem = await repository.create(input)
36
+
37
+ const updatedInput = { title: 'Updated Book', author: 'Updated Author', publishedDate: new Date() }
38
+ const updatedItem = await repository.update({ id: createdItem.id }, updatedInput)
39
+
40
+ expect(updatedItem).toEqual({ id: createdItem.id, ...updatedInput })
41
+ expect(await repository.load({ id: createdItem.id })).toEqual({ id: createdItem.id, ...updatedInput })
42
+ })
43
+
44
+ it('should throw an error when updating a non-existent item', async () => {
45
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
46
+
47
+ await expect(repository.update({ id: 999 }, input)).rejects.toThrow('Item not found')
48
+ })
49
+
50
+ it('should remove an item', async () => {
51
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
52
+ const createdItem = await repository.create(input)
53
+
54
+ const removedItem = await repository.remove({ id: createdItem.id })
55
+
56
+ expect(removedItem).toEqual(input)
57
+ const loadedItem = await repository.load({ id: createdItem.id })
58
+ expect(loadedItem).toBeNull()
59
+ })
60
+
61
+ it('should restore a removed item', async () => {
62
+ const input = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
63
+ const createdItem = await repository.create(input)
64
+ await repository.remove({ id: createdItem.id })
65
+
66
+ const restoredItem = await repository.restore({ id: createdItem.id })
67
+
68
+ expect(restoredItem).toEqual(input)
69
+ expect(await repository.load({ id: createdItem.id })).toEqual(input)
70
+ })
71
+
72
+ it('should throw an error when restoring a non-existent item', async () => {
73
+ await expect(repository.restore({ id: 999 })).rejects.toThrow('Item not found in trash')
74
+ })
75
+
76
+ it('should allow me to create an item without a primary key', async () => {
77
+ const input = { title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
78
+ const createdItem = await repository.create(input)
79
+
80
+ expect(createdItem.id).toBeDefined()
81
+ expect(createdItem.title).toBe(input.title)
82
+ expect(createdItem.author).toBe(input.author)
83
+ expect(await repository.load({ id: createdItem.id })).toEqual(createdItem)
84
+ })
85
+
86
+ it('should increment ids when creating items without a primary key', async () => {
87
+ const input1 = { title: 'Test Book 1', author: 'Author Name 1', publishedDate: new Date() }
88
+ const input2 = { title: 'Test Book 2', author: 'Author Name 2', publishedDate: new Date() }
89
+
90
+ const createdItem1 = await repository.create(input1)
91
+ const createdItem2 = await repository.create(input2)
92
+
93
+ expect(createdItem1.id).toBe(1)
94
+ expect(createdItem2.id).toBe(2)
95
+ expect(await repository.load({ id: createdItem1.id })).toEqual(createdItem1)
96
+ expect(await repository.load({ id: createdItem2.id })).toEqual(createdItem2)
97
+ })
98
+
99
+ it('should return null when loading a non-existent item', async () => {
100
+ const result = await repository.load({ id: 999 })
101
+ expect(result).toBeNull()
102
+ })
103
+
104
+ it('should be able to load items from a custom filter', async () => {
105
+ // Creating a hypothetical schema with a custom filter, and a title lookup attribute
106
+ const repository = new MockMemoryRepository({
107
+ schema: mockSchema.custom({
108
+ lookup: (h) =>
109
+ new ZodModel(
110
+ h.name,
111
+ z.object({
112
+ id: z.number().optional(),
113
+ title: z.string().optional(),
114
+ }),
115
+ ),
116
+ }),
117
+ lookup: (data, lookup) =>
118
+ data.id === lookup.id || data.title?.toLowerCase() === lookup.title?.toLowerCase(),
119
+ })
120
+
121
+ const input = { title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
122
+ const createdItem = await repository.create(input)
123
+
124
+ const loadedItem = await repository.load({ id: createdItem.id })
125
+ const titleLoadedItem = await repository.load({ title: createdItem.title })
126
+ expect(loadedItem).toEqual(createdItem)
127
+ expect(titleLoadedItem).toEqual(createdItem)
128
+ })
129
+ })