Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,34 @@ videos.defineRelations(({ one }) => ({

> In this example, `post.attachments` is an array of either `images` or `videos`, where records from both collections are allowed.

## Error handling

Data provides multiple different error classes to help you differentiate and handle different errors.

### `OperationError`

- `code` `<OperationErrorCode>`, the error code describing the failed operation;
- `cause` `<Error>` (_optional_), a reference to the original thrown error.

Thrown whenever performing a record operation fails. For example:

- When creating a new record whose initial values do not match the collection's schema;
- When there are no records found for a strict query.

### `RelationError`

- `code` `<RelationErrorCode>`, the error code describing the relation operation;
- `info` `<object>`, additional error information;
- `path` `<PropertyPath>`, path of the relational property;
- `ownerCollection` `<Collection>`, a reference to the owner collection;
- `foreignCollection` `<Array<Collection>>`, an array of foreign collections referenced by this relation;
- `options` `RelationDefinitionOptions`, the options object passed upon decaring this relation.

Thrown whenever performing a relation operation fails. For example:

- When attempting to reference a foreign record that's already associated with another record in a unique relation;
- When directly assigning value to a relational property.

---

## API
Expand Down
28 changes: 14 additions & 14 deletions src/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from '#/src/utils.js'
import { type SortOptions, sortResults } from '#/src/sort.js'
import type { Extension } from '#/src/extensions/index.js'
import { OperationError, StrictOperationError } from '#/src/errors.js'
import { OperationError, OperationErrorCodes } from '#/src/errors.js'
import { TypedEvent, type Emitter } from 'rettime'

let collectionsCreated = 0
Expand Down Expand Up @@ -131,16 +131,17 @@ export class Collection<Schema extends StandardSchemaV1> {

if (validationResult.issues) {
console.error(validationResult.issues)
throw new InvariantError(
'Failed to create a new record with initial values (%j): does not match the schema',
initialValues,

throw new OperationError(
'Failed to create a new record with initial values: does not match the schema. Please see the schema validation errors above.',
OperationErrorCodes.INVALID_INITIAL_VALUES,
)
}

let record = validationResult.value as RecordType

invariant.as(
OperationError.for('create', { initialValues }),
OperationError.for(OperationErrorCodes.INVALID_INITIAL_VALUES),
typeof record === 'object',
'Failed to create a record with initial values (%j): expected the record to be an object or an array',
initialValues,
Expand Down Expand Up @@ -207,8 +208,7 @@ export class Collection<Schema extends StandardSchemaV1> {
return await Promise.all(pendingPromises).catch((error) => {
throw new OperationError(
'Failed to execute "createMany" on collection: unexpected error',
'createMany',
{ count, initialValuesFactory },
OperationErrorCodes.UNEXPECTED_ERROR,
error,
)
})
Expand All @@ -232,7 +232,7 @@ export class Collection<Schema extends StandardSchemaV1> {
const firstRecord = this.#records[0]

invariant.as(
StrictOperationError.for('findFirst', { predicate, options }),
OperationError.for(OperationErrorCodes.STRICT_QUERY_WITHOUT_RESULTS),
options?.strict ? firstRecord != null : true,
'Failed to execute "findFirst" on collection without a query: the collection is empty',
)
Expand All @@ -245,7 +245,7 @@ export class Collection<Schema extends StandardSchemaV1> {
).next().value

invariant.as(
StrictOperationError.for('findFirst', { predicate, options }),
OperationError.for(OperationErrorCodes.STRICT_QUERY_WITHOUT_RESULTS),
options?.strict ? result != null : true,
'Failed to execute "findFirst" on collection: no record found matching the query',
)
Expand Down Expand Up @@ -277,7 +277,7 @@ export class Collection<Schema extends StandardSchemaV1> {
)

invariant.as(
StrictOperationError.for('findMany', { predicate, options }),
OperationError.for(OperationErrorCodes.STRICT_QUERY_WITHOUT_RESULTS),
options?.strict ? results.length > 0 : true,
'Failed to execute "findMany" on collection: no records found matching the query',
)
Expand Down Expand Up @@ -324,7 +324,7 @@ export class Collection<Schema extends StandardSchemaV1> {

if (prevRecord == null) {
invariant.as(
StrictOperationError.for('update', { predicate, options }),
OperationError.for(OperationErrorCodes.STRICT_QUERY_WITHOUT_RESULTS),
!options.strict,
'Failed to execute "update" on collection: no record found matching the query',
)
Expand Down Expand Up @@ -363,7 +363,7 @@ export class Collection<Schema extends StandardSchemaV1> {

if (prevRecords.length === 0) {
invariant.as(
StrictOperationError.for('updateMany', { predicate, options }),
OperationError.for(OperationErrorCodes.STRICT_QUERY_WITHOUT_RESULTS),
!options.strict,
'Failed to execute "updateMany" on collection: no records found matching the query',
)
Expand Down Expand Up @@ -409,7 +409,7 @@ export class Collection<Schema extends StandardSchemaV1> {

if (record == null) {
invariant.as(
StrictOperationError.for('delete', { predicate, options }),
OperationError.for(OperationErrorCodes.STRICT_QUERY_WITHOUT_RESULTS),
!options?.strict,
'Failed to execute "delete" on collection: no record found matching the query',
)
Expand Down Expand Up @@ -445,7 +445,7 @@ export class Collection<Schema extends StandardSchemaV1> {

if (records.length === 0) {
invariant.as(
StrictOperationError.for('deleteMany', { predicate, options }),
OperationError.for(OperationErrorCodes.STRICT_QUERY_WITHOUT_RESULTS),
!options?.strict,
'Failed to execute "deleteMany" on collection: no records found matching the query',
)
Expand Down
92 changes: 42 additions & 50 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,56 @@
import { InvariantError } from 'outvariant'
import type { Collection } from './collection.js'
import type { Collection } from '#/src/collection.js'
import type { PropertyPath } from '#/src/utils.js'
import type { RelationDeclarationOptions } from '#/src/relation.js'

export interface OperationErrorMap {
create: {
initialValues: Parameters<InstanceType<typeof Collection>['create']>[0]
}
createMany: {
count: number
initialValuesFactory: Parameters<
InstanceType<typeof Collection>['createMany']
>[1]
}
findFirst: {
predicate: Parameters<InstanceType<typeof Collection>['findFirst']>[0]
options: Parameters<InstanceType<typeof Collection>['findFirst']>[1]
}
findMany: {
predicate: Parameters<InstanceType<typeof Collection>['findMany']>[0]
options: Parameters<InstanceType<typeof Collection>['findMany']>[1]
}
update: {
predicate: Parameters<InstanceType<typeof Collection>['update']>[0]
options: Parameters<InstanceType<typeof Collection>['update']>[1]
}
updateMany: {
predicate: Parameters<InstanceType<typeof Collection>['updateMany']>[0]
options: Parameters<InstanceType<typeof Collection>['updateMany']>[1]
}
delete: {
predicate: Parameters<InstanceType<typeof Collection>['delete']>[0]
options: Parameters<InstanceType<typeof Collection>['delete']>[1]
}
deleteMany: {
predicate: Parameters<InstanceType<typeof Collection>['deleteMany']>[0]
options: Parameters<InstanceType<typeof Collection>['deleteMany']>[1]
}
export enum OperationErrorCodes {
UNEXPECTED_ERROR = 'UNEXPECTED_ERROR',
INVALID_INITIAL_VALUES = 'INVALID_INITIAL_VALUES',
STRICT_QUERY_WITHOUT_RESULTS = 'STRICT_QUERY_WITHOUT_RESULTS',
}

export class OperationError<
OperationName extends keyof OperationErrorMap,
> extends InvariantError {
static for<OperationName extends keyof OperationErrorMap>(
operationName: OperationName,
info: OperationErrorMap[OperationName],
) {
return (message: string) => new OperationError(message, operationName, info)
export class OperationError extends Error {
static for(code: OperationErrorCodes) {
return (message: string) => {
return new OperationError(message, code)
}
}

constructor(
message: string,
public readonly operationName: OperationName,
public readonly info: OperationErrorMap[OperationName],
public readonly code: OperationErrorCodes,
public readonly cause?: unknown,
) {
super(message)
}
}

export class StrictOperationError<
OperationName extends keyof OperationErrorMap,
> extends OperationError<OperationName> {
static for = OperationError.for
export enum RelationErrorCodes {
RELATION_NOT_READY = 'RELATION_NOT_READY',
UNEXPECTED_SET_EXPRESSION = 'UNEXPECTED_SET_EXPRESSION',
INVALID_FOREIGN_RECORD = 'INVALID_FOREIGN_RECORD',
FORBIDDEN_UNIQUE_CREATE = 'FORBIDDEN_UNIQUE_CREATE',
FORBIDDEN_UNIQUE_UPDATE = 'FORBIDDEN_UNIQUE_UPDATE',
}

export interface RelationErrorDetails {
path: PropertyPath
ownerCollection: Collection<any>
foreignCollections: Array<Collection<any>>
options: RelationDeclarationOptions
}

export class RelationError extends Error {
static for(code: RelationErrorCodes, details: RelationErrorDetails) {
return (message: string) => {
return new RelationError(message, code, details)
}
}

constructor(
message: string,
public readonly code: RelationErrorCodes,
public readonly details: RelationErrorDetails,
) {
super(message)
}
}
5 changes: 3 additions & 2 deletions src/extensions/persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
kRelationMap,
type RecordType,
} from '#/src/collection.js'
import { Logger } from '../logger.js'
import { Logger } from '#/src/logger.js'
import type { PropertyPath } from '#/src/utils.js'

const STORAGE_KEY = 'msw/data/storage'
const METADATA_KEY = '__metadata__'
Expand All @@ -27,7 +28,7 @@ export interface SerializedRecord {
interface RecordMetadata {
primaryKey: string
relations: Array<{
path: Array<string>
path: PropertyPath
foreignKeys: Array<string>
}>
}
Expand Down
9 changes: 7 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
export { Collection, type CollectionOptions } from './collection.js'
export { Query, type Condition, type PredicateFunction } from './query.js'
export { Relation } from './relation.js'
export { Relation, type RelationDeclarationOptions } from './relation.js'
export type { HookEventMap, HookEventListener } from './hooks.js'
export { OperationError, StrictOperationError } from './errors.js'
export {
OperationError,
RelationError,
RelationErrorCodes,
type RelationErrorDetails,
} from './errors.js'
Loading