• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

monolithst / functional-models / 17979447348

24 Sep 2025 02:11PM UTC coverage: 96.953% (-0.7%) from 97.611%
17979447348

push

github

web-flow
Merge pull request #38 from monolithst/fk-property

Fk property

445 of 478 branches covered (93.1%)

Branch coverage included in aggregate %.

4 of 10 new or added lines in 1 file covered. (40.0%)

1 existing line in 1 file now uncovered.

987 of 999 relevant lines covered (98.8%)

313.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.0
/src/orm/models.ts
1
import merge from 'lodash/merge'
3,177✔
2
import { asyncMap } from 'modern-async'
3✔
3
import {
4
  ModelFactory,
5
  ModelInstanceFetcher,
6
  PrimaryKeyType,
7
  DataDescription,
8
  ModelInstance,
9
  CreateParams,
10
  PropertyInstance,
11
  ToObjectResult,
12
  ModelFactoryOptions,
13
} from '../types'
14
import { Model as functionalModel } from '../models'
3✔
15
import { ValidationError } from '../errors'
3✔
16
import { uniqueTogether } from './validation'
3✔
17
import {
18
  OrmModelInstance,
19
  OrmModel,
20
  DatastoreAdapter,
21
  OrmSearch,
22
  OrmModelFactory,
23
  Orm,
24
  OrmModelExtensions,
25
  OrmModelInstanceExtensions,
26
  OrmModelFactoryOptionsExtensions,
27
  OrmSearchResult,
28
  MinimumOrmModelDefinition,
29
} from './types'
30
import { queryBuilder } from './query'
3✔
31

32
/**
33
 * Creates a structure that has an OrmModel and a fetcher
34
 * @param datastoreAdapter - A backing datastore.
35
 * @param Model - An optional underlying Model factory for using. Defaults to {@link Model}
36
 */
37
const createOrm = ({
6!
38
  datastoreAdapter,
39
  Model = functionalModel,
75✔
40
}: Readonly<{
41
  datastoreAdapter: DatastoreAdapter
42
  Model?: ModelFactory
43
}>): Orm => {
44
  if (!datastoreAdapter) {
126✔
45
    throw new Error(`Must include a datastoreAdapter`)
3✔
46
  }
47

48
  const _retrievedObjToModel =
49
    <TData extends DataDescription>(model: OrmModel<TData>) =>
123✔
50
    (obj: ToObjectResult<TData>) => {
42✔
51
      return model.create(obj as unknown as CreateParams<TData, ''>)
45✔
52
    }
53

54
  // @ts-ignore
55
  const fetcher: ModelInstanceFetcher<
56
    OrmModelExtensions,
57
    OrmModelInstanceExtensions
58
  > = async <TData extends DataDescription>(
123✔
59
    model: OrmModel<TData>,
60
    id: PrimaryKeyType
61
  ) => {
62
    const x: Promise<OrmModelInstance<TData> | undefined> = retrieve<TData>(
6✔
63
      model,
64
      id
65
    )
66
    return x
6✔
67
  }
68

69
  const retrieve = async <T extends DataDescription>(
123✔
70
    model: OrmModel<T>,
71
    id: PrimaryKeyType
72
  ) => {
73
    const obj = await datastoreAdapter.retrieve(model, id)
9✔
74
    if (!obj) {
9✔
75
      return undefined
6✔
76
    }
77
    return _retrievedObjToModel<T>(model)(obj)
3✔
78
  }
79

80
  const _defaultOptions = <
123✔
81
    T extends DataDescription,
82
  >(): ModelFactoryOptions<T> => ({
117✔
83
    instanceCreatedCallback: undefined,
84
  })
85

86
  const _convertOptions = <T extends DataDescription>(
123✔
87
    options?: ModelFactoryOptions<T, OrmModelFactoryOptionsExtensions>
88
  ) => {
89
    const r: ModelFactoryOptions<T, OrmModelFactoryOptionsExtensions> = merge(
117✔
90
      {},
91
      _defaultOptions(),
92
      options
93
    )
94
    return r
117✔
95
  }
96

97
  const ThisModel: OrmModelFactory = <T extends DataDescription>(
123✔
98
    modelDefinition: MinimumOrmModelDefinition<T>,
99
    options?: ModelFactoryOptions<T, OrmModelFactoryOptionsExtensions>
100
  ) => {
101
    /*
102
    NOTE: We need access to the model AFTER its built, so we have to have this state variable.
103
    It has been intentionally decided that recreating the model each and every time for each database retrieve is
104
    too much cost to obtain "functional purity". This could always be reverted back.
105
    */
106
    // @ts-ignore
107
    // eslint-disable-next-line functional/no-let
108
    let model: OrmModel<T, OrmModelExtensions, OrmModelInstanceExtensions> =
109
      null
117✔
110
    const theOptions = _convertOptions(options)
117✔
111

112
    const search = <TOverride extends DataDescription>(
117✔
113
      ormQuery: OrmSearch
114
    ): Promise<OrmSearchResult<TOverride>> => {
115
      return datastoreAdapter.search(model, ormQuery).then(result => {
24✔
116
        // @ts-ignore
117
        const conversionFunc = _retrievedObjToModel<TOverride>(model)
24✔
118
        return {
24✔
119
          // @ts-ignore
120
          instances: result.instances.map(conversionFunc),
121
          page: result.page,
122
        }
123
      })
124
    }
125

126
    const searchOne = <TOverride extends DataDescription>(
117✔
127
      ormQuery: OrmSearch
128
    ) => {
129
      ormQuery = merge(ormQuery, { take: 1 })
3✔
130
      return search<TOverride>(ormQuery).then(({ instances }) => {
3✔
131
        return instances[0]
3✔
132
      })
133
    }
134

135
    const bulkInsert = async <TOverride extends DataDescription>(
117✔
136
      instances: readonly OrmModelInstance<TOverride>[]
137
    ) => {
138
      if (datastoreAdapter.bulkInsert) {
6✔
139
        // @ts-ignore
140
        await datastoreAdapter.bulkInsert<TOverride>(model, instances)
3✔
141
        return undefined
3✔
142
      }
143
      await asyncMap(instances, x => x.save())
12✔
144
      return undefined
3✔
145
    }
146

147
    const bulkDelete = async <TOverride extends DataDescription>(
117✔
148
      keysOrInstances:
149
        | readonly OrmModelInstance<TOverride>[]
150
        | readonly PrimaryKeyType[]
151
    ) => {
152
      const ids: readonly PrimaryKeyType[] =
NEW
153
        typeof keysOrInstances[0] === 'object'
×
154
          ? keysOrInstances.map(x =>
NEW
155
              (x as OrmModelInstance<TOverride>).getPrimaryKey()
×
156
            )
157
          : (keysOrInstances as readonly PrimaryKeyType[])
NEW
158
      if (datastoreAdapter.bulkDelete) {
×
NEW
159
        await datastoreAdapter.bulkDelete(model, ids)
×
NEW
160
        return undefined
×
161
      }
NEW
162
      await asyncMap(ids, id => model.delete(id))
×
UNCOV
163
      return undefined
×
164
    }
165

166
    const loadedRetrieve = <TOverride extends DataDescription>(
117✔
167
      id: PrimaryKeyType
168
    ) => {
169
      // @ts-ignore
170
      return retrieve<TOverride>(model, id)
3✔
171
    }
172

173
    const modelValidators = modelDefinition?.uniqueTogether
117✔
174
      ? (modelDefinition.modelValidators || []).concat(
3!
175
          // @ts-ignore
176
          uniqueTogether(modelDefinition.uniqueTogether)
177
        )
178
      : modelDefinition.modelValidators
179

180
    const ormModelDefinition = merge({}, modelDefinition, {
117✔
181
      modelValidators,
182
    })
183

184
    const _updateLastModifiedIfExistsReturnNewObj = async <
117✔
185
      TOverride extends DataDescription,
186
    >(
187
      instance: ModelInstance<TOverride>
188
    ): Promise<OrmModelInstance<TOverride>> => {
189
      const hasLastModified = Object.entries(
15✔
190
        instance.getModel().getModelDefinition().properties
191
      ).filter(propertyEntry => {
192
        const property = propertyEntry[1] as PropertyInstance<any>
30✔
193
        return Boolean('lastModifiedUpdateMethod' in property)
30✔
194
      })[0]
195

196
      const doLastModified = async () => {
15✔
197
        const obj = await instance.toObj<TOverride>()
3✔
198
        const newInstance = model.create(
3✔
199
          // @ts-ignore
200
          merge(obj, {
201
            [hasLastModified[0]]:
202
              // @ts-ignore
203
              hasLastModified[1].lastModifiedUpdateMethod(),
204
          })
205
        )
206
        return newInstance
3✔
207
      }
208

209
      // @ts-ignore
210
      return hasLastModified ? doLastModified() : instance
15✔
211
    }
212

213
    const save = async <TOverride extends DataDescription>(
117✔
214
      instance: ModelInstance<TOverride>
215
    ): Promise<OrmModelInstance<TOverride>> => {
216
      return Promise.resolve().then(async () => {
15✔
217
        const newInstance =
218
          await _updateLastModifiedIfExistsReturnNewObj<TOverride>(instance)
15✔
219
        const invalid = await newInstance.validate()
15✔
220
        if (invalid) {
15✔
221
          throw new ValidationError(model.getName(), invalid)
3✔
222
        }
223
        const savedObj = await datastoreAdapter.save<TOverride>(newInstance)
12✔
224
        // @ts-ignore
225
        return _retrievedObjToModel<TOverride>(model)(savedObj)
12✔
226
      })
227
    }
228

229
    const createAndSave = async <TOverride extends DataDescription>(
117✔
230
      data: ModelInstance<TOverride>
231
    ): Promise<OrmModelInstance<TOverride>> => {
232
      if (datastoreAdapter.createAndSave) {
6✔
233
        const response = await datastoreAdapter.createAndSave<TOverride>(data)
3✔
234
        // @ts-ignore
235
        return _retrievedObjToModel<TOverride>(model)(response)
3✔
236
      }
237
      // @ts-ignore
238
      const instance = model.create(await data.toObj<TOverride>())
3✔
239
      return instance.save()
3✔
240
    }
241

242
    const deleteObj = (id: PrimaryKeyType) => {
117✔
243
      return Promise.resolve().then(async () => {
3✔
244
        await datastoreAdapter.delete(model, id)
3✔
245
      })
246
    }
247

248
    const _getSave = (
117✔
249
      instance: ModelInstance<T>
250
    ): (<TOverrides extends DataDescription>() => Promise<
251
      OrmModelInstance<TOverrides>
252
    >) => {
253
      // See if save has been overrided.
254
      if (theOptions.save !== undefined) {
366✔
255
        // @ts-ignore
256
        return () => theOptions.save(save, instance)
6✔
257
      }
258
      // @ts-ignore
259
      return () => save(instance)
360✔
260
    }
261

262
    const _getDelete = (instance: ModelInstance<T>) => {
117✔
263
      if (theOptions.delete) {
366✔
264
        // @ts-ignore
265
        return () => theOptions.delete(deleteObj, instance)
6✔
266
      }
267
      return () => deleteObj(instance.getPrimaryKey())
360✔
268
    }
269

270
    const instanceCreatedCallback = (instance: OrmModelInstance<T>) => {
117✔
271
      // @ts-ignore
272
      // eslint-disable-next-line functional/immutable-data
273
      instance.save = _getSave(instance)
183✔
274
      // @ts-ignore
275
      // eslint-disable-next-line functional/immutable-data
276
      instance.delete = _getDelete(instance)
183✔
277
      if (theOptions.instanceCreatedCallback) {
183✔
278
        const callbacks: readonly ((instance: OrmModelInstance<T>) => void)[] =
279
          Array.isArray(theOptions.instanceCreatedCallback)
6✔
280
            ? theOptions.instanceCreatedCallback
281
            : [theOptions.instanceCreatedCallback]
282
        callbacks.forEach(x => x(instance))
6✔
283
      }
284
    }
285

286
    // Absolutely do not put theOptions as the first argument. This first argument is what is modified,
287
    // therefore the instanceCreatedCallback keeps calling itself instead of wrapping.
288
    const overridedOptions: ModelFactoryOptions<
117✔
289
      T,
290
      OrmModelFactoryOptionsExtensions
291
    > = merge({}, theOptions, {
292
      instanceCreatedCallback: [instanceCreatedCallback],
293
    })
294

295
    const baseModel = Model<T>(ormModelDefinition, overridedOptions)
117✔
296
    const lowerLevelCreate = baseModel.create
117✔
297

298
    const _convertModelInstance = (
117✔
299
      instance: ModelInstance<T>
300
    ): OrmModelInstance<T> => {
301
      return merge(instance, {
183✔
302
        create,
303
        getModel: () => model,
57✔
304
        save: _getSave(instance),
305
        delete: _getDelete(instance),
306
      })
307
    }
308

309
    const create = <IgnoreProperties extends string = ''>(
117✔
310
      data: CreateParams<T, IgnoreProperties>
311
    ): OrmModelInstance<T> => {
312
      const result = lowerLevelCreate(data)
183✔
313
      return _convertModelInstance(result)
183✔
314
    }
315

316
    const _countRecursive = async (page = null): Promise<number> => {
117✔
317
      const results = await model.search(
9✔
318
        queryBuilder().pagination(page).compile()
319
      )
320
      const length1 = results.instances.length
9✔
321
      // Don't run it again if the page is the same as a previous run.
322
      if (results.page && results.page !== page) {
9✔
323
        const length2 = await _countRecursive(results.page)
3✔
324
        return length1 + length2
3✔
325
      }
326
      return length1
6✔
327
    }
328

329
    const count = async (): Promise<number> => {
117✔
330
      // NOTE: This is EXTREMELY inefficient. This should be
331
      // overrided by a dataProvider if at all possible.
332
      if (datastoreAdapter.count) {
9✔
333
        return datastoreAdapter.count<T>(model)
3✔
334
      }
335
      return _countRecursive()
6✔
336
    }
337

338
    model = merge(baseModel, {
117✔
339
      create,
340
      save,
341
      delete: deleteObj,
342
      retrieve: loadedRetrieve,
343
      search,
344
      searchOne,
345
      createAndSave,
346
      bulkInsert,
347
      bulkDelete,
348
      count,
349
    })
350
    return model
117✔
351
  }
352

353
  return {
123✔
354
    Model: ThisModel,
355
    fetcher,
356
  }
357
}
358

359
export { createOrm }
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc