@deessejs/collections 0.0.1
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/package.json +60 -0
- package/src/adapter.ts +38 -0
- package/src/collection.ts +138 -0
- package/src/config.ts +134 -0
- package/src/field-type.ts +40 -0
- package/src/field.ts +46 -0
- package/src/fields/f.ts +192 -0
- package/src/fields/index.ts +1 -0
- package/src/index.ts +27 -0
- package/src/migrations.ts +49 -0
- package/src/operations/collection-operations.ts +808 -0
- package/src/operations/index.ts +2 -0
- package/src/operations/types.ts +141 -0
- package/src/schema.ts +62 -0
- package/tests/adapter.test.ts +35 -0
- package/tests/collection.test.ts +205 -0
- package/tests/config.test.ts +58 -0
- package/tests/field-type.test.ts +181 -0
- package/tests/field.test.ts +201 -0
- package/tests/fixtures.ts +44 -0
- package/tests/hooks.test.ts +1076 -0
- package/tests/integration/hooks.test.ts +329 -0
- package/tests/metadata.test.ts +200 -0
- package/tests/schema.test.ts +58 -0
- package/tests/type-inference.test.ts +108 -0
- package/tsconfig.json +32 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.ts +29 -0
- package/vitest.integration.config.ts +9 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract field names from a collection's fields
|
|
3
|
+
*/
|
|
4
|
+
export type CollectionFieldNames<T extends Record<string, { fieldType: { schema: unknown } }>> = keyof T
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract field schema type from a field definition
|
|
8
|
+
*/
|
|
9
|
+
export type FieldSchema<T extends { fieldType: { schema: unknown } }> = T['fieldType']['schema']
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Where condition operators
|
|
13
|
+
*/
|
|
14
|
+
export type WhereOperator<T> =
|
|
15
|
+
| { eq: T }
|
|
16
|
+
| { neq: T }
|
|
17
|
+
| { gt: T }
|
|
18
|
+
| { gte: T }
|
|
19
|
+
| { lt: T }
|
|
20
|
+
| { lte: T }
|
|
21
|
+
| { in: T[] }
|
|
22
|
+
| { notIn: T[] }
|
|
23
|
+
| { contains: T }
|
|
24
|
+
| { startsWith: T }
|
|
25
|
+
| { endsWith: T }
|
|
26
|
+
| { isNull: boolean }
|
|
27
|
+
| { not: T }
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Where condition value
|
|
31
|
+
*/
|
|
32
|
+
export type WhereValue<T> = T | WhereOperator<T>
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Where conditions
|
|
36
|
+
*/
|
|
37
|
+
export type WhereConditions<T = Record<string, unknown>> = T
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Order by direction
|
|
41
|
+
*/
|
|
42
|
+
export type OrderByDirection = 'asc' | 'desc'
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Order by clause
|
|
46
|
+
*/
|
|
47
|
+
export type OrderByClause<T = Record<string, unknown>> = T
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Select clause - fields to return
|
|
51
|
+
*/
|
|
52
|
+
export type SelectClause<T = Record<string, unknown>> = Partial<Record<keyof T, boolean>>
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find many options
|
|
56
|
+
*/
|
|
57
|
+
export type FindManyOptions<T = Record<string, unknown>> = {
|
|
58
|
+
where?: WhereConditions<T>
|
|
59
|
+
orderBy?: OrderByClause<T> | OrderByClause<T>[]
|
|
60
|
+
limit?: number
|
|
61
|
+
offset?: number
|
|
62
|
+
select?: SelectClause<T>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Find unique options
|
|
67
|
+
*/
|
|
68
|
+
export type FindUniqueOptions<T = Record<string, unknown>> = {
|
|
69
|
+
where: WhereConditions<T>
|
|
70
|
+
select?: SelectClause<T>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Find first options
|
|
75
|
+
*/
|
|
76
|
+
export type FindFirstOptions<T = Record<string, unknown>> = {
|
|
77
|
+
where: WhereConditions<T>
|
|
78
|
+
orderBy?: OrderByClause<T> | OrderByClause<T>[]
|
|
79
|
+
select?: SelectClause<T>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create options
|
|
84
|
+
*/
|
|
85
|
+
export type CreateOptions<T> = {
|
|
86
|
+
data: T
|
|
87
|
+
returning?: boolean
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create many options
|
|
92
|
+
*/
|
|
93
|
+
export type CreateManyOptions<T> = {
|
|
94
|
+
data: T[]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Update options
|
|
99
|
+
*/
|
|
100
|
+
export type UpdateOptions<T, TFields = Record<string, unknown>> = {
|
|
101
|
+
where: WhereConditions<TFields>
|
|
102
|
+
data: Partial<T>
|
|
103
|
+
returning?: boolean
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Update many options
|
|
108
|
+
*/
|
|
109
|
+
export type UpdateManyOptions<T, TFields = Record<string, unknown>> = {
|
|
110
|
+
where: WhereConditions<TFields>
|
|
111
|
+
data: Partial<T>
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Delete options
|
|
116
|
+
*/
|
|
117
|
+
export type DeleteOptions<TFields = Record<string, unknown>> = {
|
|
118
|
+
where: WhereConditions<TFields>
|
|
119
|
+
returning?: boolean
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Delete many options
|
|
124
|
+
*/
|
|
125
|
+
export type DeleteManyOptions<TFields = Record<string, unknown>> = {
|
|
126
|
+
where: WhereConditions<TFields>
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Count options
|
|
131
|
+
*/
|
|
132
|
+
export type CountOptions<TFields = Record<string, unknown>> = {
|
|
133
|
+
where?: WhereConditions<TFields>
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Exists options
|
|
138
|
+
*/
|
|
139
|
+
export type ExistsOptions<TFields = Record<string, unknown>> = {
|
|
140
|
+
where: WhereConditions<TFields>
|
|
141
|
+
}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { pgTable, serial, text, timestamp, uuid, varchar, boolean, integer } from 'drizzle-orm/pg-core'
|
|
2
|
+
|
|
3
|
+
import type { Collection } from './collection'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build Drizzle table from collection definition
|
|
7
|
+
*/
|
|
8
|
+
export const buildTable = (collection: Collection) => {
|
|
9
|
+
// Build columns object
|
|
10
|
+
const columns: Record<string, unknown> = {
|
|
11
|
+
// Add default id column
|
|
12
|
+
id: serial('id').primaryKey()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Build columns from fields
|
|
16
|
+
for (const [fieldName, fieldDef] of Object.entries(collection.fields)) {
|
|
17
|
+
// Skip the id field if explicitly defined in fields
|
|
18
|
+
if (fieldName === 'id') continue
|
|
19
|
+
|
|
20
|
+
const fieldType = fieldDef.fieldType as { name?: string; type?: string }
|
|
21
|
+
const fieldTypeName = fieldType.name || fieldType.type || 'text'
|
|
22
|
+
|
|
23
|
+
switch (fieldTypeName) {
|
|
24
|
+
case 'text':
|
|
25
|
+
columns[fieldName] = text(fieldName)
|
|
26
|
+
break
|
|
27
|
+
case 'varchar':
|
|
28
|
+
columns[fieldName] = varchar(fieldName, { length: 255 })
|
|
29
|
+
break
|
|
30
|
+
case 'number':
|
|
31
|
+
case 'integer':
|
|
32
|
+
columns[fieldName] = integer(fieldName)
|
|
33
|
+
break
|
|
34
|
+
case 'boolean':
|
|
35
|
+
columns[fieldName] = boolean(fieldName)
|
|
36
|
+
break
|
|
37
|
+
case 'timestamp':
|
|
38
|
+
columns[fieldName] = timestamp(fieldName)
|
|
39
|
+
break
|
|
40
|
+
case 'uuid':
|
|
41
|
+
columns[fieldName] = uuid(fieldName)
|
|
42
|
+
break
|
|
43
|
+
default:
|
|
44
|
+
columns[fieldName] = text(fieldName)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return pgTable(collection.slug, columns as Record<string, ReturnType<typeof text>>)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build all tables from collections
|
|
53
|
+
*/
|
|
54
|
+
export const buildSchema = (collections: Collection[]) => {
|
|
55
|
+
const tables: Record<string, ReturnType<typeof pgTable>> = {}
|
|
56
|
+
|
|
57
|
+
for (const coll of collections) {
|
|
58
|
+
tables[coll.slug] = buildTable(coll)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return tables
|
|
62
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { pgAdapter, type PgAdapter, type DatabaseAdapter } from '../src'
|
|
3
|
+
import { testAdapter } from './fixtures'
|
|
4
|
+
|
|
5
|
+
describe('pgAdapter', () => {
|
|
6
|
+
it('creates a postgres adapter with url', () => {
|
|
7
|
+
const adapter = pgAdapter({ url: 'postgres://user:pass@localhost:5432/db' })
|
|
8
|
+
|
|
9
|
+
expect(adapter.type).toBe('postgres')
|
|
10
|
+
expect(adapter.config.url).toBe('postgres://user:pass@localhost:5432/db')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('creates adapter with default migrations path', () => {
|
|
14
|
+
const adapter = pgAdapter({ url: 'postgres://localhost:5432/db' })
|
|
15
|
+
|
|
16
|
+
expect(adapter.config.migrationsPath).toBe('./migrations')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('creates adapter with custom migrations path', () => {
|
|
20
|
+
const adapter = pgAdapter({
|
|
21
|
+
url: 'postgres://localhost:5432/db',
|
|
22
|
+
migrationsPath: './custom-migrations'
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
expect(adapter.config.migrationsPath).toBe('./custom-migrations')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('has correct type', () => {
|
|
29
|
+
const _typeCheck: PgAdapter = testAdapter
|
|
30
|
+
const _dbTypeCheck: DatabaseAdapter = testAdapter
|
|
31
|
+
|
|
32
|
+
expect(_typeCheck).toBeDefined()
|
|
33
|
+
expect(_dbTypeCheck).toBeDefined()
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { collection } from '../src/collection'
|
|
3
|
+
import { field } from '../src/field'
|
|
4
|
+
import { f } from '../src'
|
|
5
|
+
|
|
6
|
+
describe('collection', () => {
|
|
7
|
+
describe('basic creation', () => {
|
|
8
|
+
it('creates a basic collection', () => {
|
|
9
|
+
const users = collection({
|
|
10
|
+
slug: 'users',
|
|
11
|
+
fields: {
|
|
12
|
+
name: field({ fieldType: f.text() }),
|
|
13
|
+
email: field({ fieldType: f.text() })
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
expect(users.slug).toBe('users')
|
|
18
|
+
expect(users.name).toBeUndefined()
|
|
19
|
+
expect(Object.keys(users.fields)).toContain('name')
|
|
20
|
+
expect(Object.keys(users.fields)).toContain('email')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('creates a collection with name', () => {
|
|
24
|
+
const users = collection({
|
|
25
|
+
slug: 'users',
|
|
26
|
+
name: 'Users',
|
|
27
|
+
fields: {
|
|
28
|
+
name: field({ fieldType: f.text() })
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
expect(users.slug).toBe('users')
|
|
33
|
+
expect(users.name).toBe('Users')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('creates a collection with various field types', () => {
|
|
37
|
+
const posts = collection({
|
|
38
|
+
slug: 'posts',
|
|
39
|
+
fields: {
|
|
40
|
+
title: field({ fieldType: f.text() }),
|
|
41
|
+
views: field({ fieldType: f.number() }),
|
|
42
|
+
published: field({ fieldType: f.boolean() }),
|
|
43
|
+
createdAt: field({ fieldType: f.date() }),
|
|
44
|
+
updatedAt: field({ fieldType: f.timestamp() }),
|
|
45
|
+
metadata: field({ fieldType: f.json() }),
|
|
46
|
+
tags: field({ fieldType: f.array(f.text()) }),
|
|
47
|
+
author: field({ fieldType: f.relation({ collection: 'users' }) })
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
expect(posts.slug).toBe('posts')
|
|
52
|
+
expect(Object.keys(posts.fields)).toHaveLength(8)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('hooks', () => {
|
|
57
|
+
it('creates a collection with all hook types', () => {
|
|
58
|
+
const mockHook = async () => {}
|
|
59
|
+
|
|
60
|
+
const users = collection({
|
|
61
|
+
slug: 'users',
|
|
62
|
+
fields: {
|
|
63
|
+
name: field({ fieldType: f.text() })
|
|
64
|
+
},
|
|
65
|
+
hooks: {
|
|
66
|
+
beforeOperation: [mockHook],
|
|
67
|
+
afterOperation: [mockHook],
|
|
68
|
+
beforeCreate: [mockHook],
|
|
69
|
+
afterCreate: [mockHook],
|
|
70
|
+
beforeUpdate: [mockHook],
|
|
71
|
+
afterUpdate: [mockHook],
|
|
72
|
+
beforeDelete: [mockHook],
|
|
73
|
+
afterDelete: [mockHook],
|
|
74
|
+
beforeRead: [mockHook],
|
|
75
|
+
afterRead: [mockHook]
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
expect(users.hooks?.beforeOperation).toHaveLength(1)
|
|
80
|
+
expect(users.hooks?.afterOperation).toHaveLength(1)
|
|
81
|
+
expect(users.hooks?.beforeCreate).toHaveLength(1)
|
|
82
|
+
expect(users.hooks?.afterCreate).toHaveLength(1)
|
|
83
|
+
expect(users.hooks?.beforeUpdate).toHaveLength(1)
|
|
84
|
+
expect(users.hooks?.afterUpdate).toHaveLength(1)
|
|
85
|
+
expect(users.hooks?.beforeDelete).toHaveLength(1)
|
|
86
|
+
expect(users.hooks?.afterDelete).toHaveLength(1)
|
|
87
|
+
expect(users.hooks?.beforeRead).toHaveLength(1)
|
|
88
|
+
expect(users.hooks?.afterRead).toHaveLength(1)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('creates a collection without hooks', () => {
|
|
92
|
+
const users = collection({
|
|
93
|
+
slug: 'users',
|
|
94
|
+
fields: {
|
|
95
|
+
name: field({ fieldType: f.text() })
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
expect(users.hooks).toBeUndefined()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('creates a collection with multiple hooks per event', () => {
|
|
103
|
+
const hook1 = async () => {}
|
|
104
|
+
const hook2 = async () => {}
|
|
105
|
+
|
|
106
|
+
const users = collection({
|
|
107
|
+
slug: 'users',
|
|
108
|
+
fields: {
|
|
109
|
+
name: field({ fieldType: f.text() })
|
|
110
|
+
},
|
|
111
|
+
hooks: {
|
|
112
|
+
beforeCreate: [hook1, hook2]
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
expect(users.hooks?.beforeCreate).toHaveLength(2)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('creates a collection with only beforeOperation and afterOperation hooks', () => {
|
|
120
|
+
const mockHook = async () => {}
|
|
121
|
+
|
|
122
|
+
const users = collection({
|
|
123
|
+
slug: 'users',
|
|
124
|
+
fields: {
|
|
125
|
+
name: field({ fieldType: f.text() })
|
|
126
|
+
},
|
|
127
|
+
hooks: {
|
|
128
|
+
beforeOperation: [mockHook],
|
|
129
|
+
afterOperation: [mockHook]
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
expect(users.hooks?.beforeOperation).toHaveLength(1)
|
|
134
|
+
expect(users.hooks?.afterOperation).toHaveLength(1)
|
|
135
|
+
expect(users.hooks?.beforeCreate).toBeUndefined()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('creates a collection with only read hooks', () => {
|
|
139
|
+
const mockHook = async () => {}
|
|
140
|
+
|
|
141
|
+
const users = collection({
|
|
142
|
+
slug: 'users',
|
|
143
|
+
fields: {
|
|
144
|
+
name: field({ fieldType: f.text() })
|
|
145
|
+
},
|
|
146
|
+
hooks: {
|
|
147
|
+
beforeRead: [mockHook],
|
|
148
|
+
afterRead: [mockHook]
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
expect(users.hooks?.beforeRead).toHaveLength(1)
|
|
153
|
+
expect(users.hooks?.afterRead).toHaveLength(1)
|
|
154
|
+
expect(users.hooks?.beforeCreate).toBeUndefined()
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('field options', () => {
|
|
159
|
+
it('creates a collection with field options', () => {
|
|
160
|
+
const users = collection({
|
|
161
|
+
slug: 'users',
|
|
162
|
+
fields: {
|
|
163
|
+
name: field({ fieldType: f.text(), required: true, label: 'Name' }),
|
|
164
|
+
email: field({ fieldType: f.text(), unique: true, indexed: true }),
|
|
165
|
+
age: field({ fieldType: f.number(), default: 18 }),
|
|
166
|
+
bio: field({ fieldType: f.text(), description: 'User biography' })
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
expect(users.fields.name.required).toBe(true)
|
|
171
|
+
expect(users.fields.name.label).toBe('Name')
|
|
172
|
+
expect(users.fields.email.unique).toBe(true)
|
|
173
|
+
expect(users.fields.email.indexed).toBe(true)
|
|
174
|
+
expect(users.fields.age.default).toBe(18)
|
|
175
|
+
expect(users.fields.bio.description).toBe('User biography')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('creates a collection with enum field', () => {
|
|
179
|
+
const posts = collection({
|
|
180
|
+
slug: 'posts',
|
|
181
|
+
fields: {
|
|
182
|
+
status: field({ fieldType: f.select(['draft', 'published', 'archived']) })
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
expect(posts.fields.status).toBeDefined()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('creates a collection with relations', () => {
|
|
190
|
+
const posts = collection({
|
|
191
|
+
slug: 'posts',
|
|
192
|
+
fields: {
|
|
193
|
+
author: field({ fieldType: f.relation({ collection: 'users' }) }),
|
|
194
|
+
category: field({ fieldType: f.relation({ collection: 'categories', singular: true }) }),
|
|
195
|
+
tags: field({ fieldType: f.relation({ collection: 'tags', many: true }) })
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
expect(posts.fields.author).toBeDefined()
|
|
200
|
+
expect(posts.fields.category).toBeDefined()
|
|
201
|
+
expect(posts.fields.tags).toBeDefined()
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { defineConfig } from '../src'
|
|
3
|
+
import { collection } from '../src/collection'
|
|
4
|
+
import { field } from '../src/field'
|
|
5
|
+
import { f } from '../src'
|
|
6
|
+
import { testAdapter, testCollections } from './fixtures'
|
|
7
|
+
|
|
8
|
+
describe('defineConfig', () => {
|
|
9
|
+
it('creates a config with a single collection', () => {
|
|
10
|
+
const config = defineConfig({
|
|
11
|
+
database: testAdapter,
|
|
12
|
+
collections: [testCollections.users]
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
expect(config.collections.users).toBeDefined()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('creates a config with multiple collections', () => {
|
|
19
|
+
const config = defineConfig({
|
|
20
|
+
database: testAdapter,
|
|
21
|
+
collections: [testCollections.users, testCollections.posts]
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
expect(config.collections.users).toBeDefined()
|
|
25
|
+
expect(config.collections.posts).toBeDefined()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('creates a config with plugins', () => {
|
|
29
|
+
const mockPlugin = {
|
|
30
|
+
name: 'test-plugin',
|
|
31
|
+
collections: {
|
|
32
|
+
settings: collection({
|
|
33
|
+
slug: 'settings',
|
|
34
|
+
fields: { key: field({ fieldType: f.text() }) }
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const config = defineConfig({
|
|
40
|
+
database: testAdapter,
|
|
41
|
+
collections: [testCollections.users],
|
|
42
|
+
plugins: [mockPlugin]
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
expect(config.collections.users).toBeDefined()
|
|
46
|
+
expect(config.collections.settings).toBeDefined()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('returns drizzle instance for db', () => {
|
|
50
|
+
const config = defineConfig({
|
|
51
|
+
database: testAdapter,
|
|
52
|
+
collections: [testCollections.users]
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
expect(config.db).not.toBeNull()
|
|
56
|
+
expect(config.db).toBeDefined()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { fieldType } from '../src/field-type'
|
|
3
|
+
import { field } from '../src/field'
|
|
4
|
+
import { f } from '../src'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
describe('fieldType', () => {
|
|
8
|
+
it('creates a basic field type', () => {
|
|
9
|
+
const textField = fieldType({
|
|
10
|
+
schema: z.string(),
|
|
11
|
+
database: { type: 'text' }
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const instance = textField()
|
|
15
|
+
|
|
16
|
+
expect(instance.schema).toBeInstanceOf(z.ZodString)
|
|
17
|
+
expect(instance.database).toEqual({ type: 'text' })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('creates field type without database config', () => {
|
|
21
|
+
const textField = fieldType({
|
|
22
|
+
schema: z.string()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const instance = textField()
|
|
26
|
+
|
|
27
|
+
expect(instance.schema).toBeInstanceOf(z.ZodString)
|
|
28
|
+
expect(instance.database).toEqual({})
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('field', () => {
|
|
33
|
+
it('creates a basic field', () => {
|
|
34
|
+
const myField = field({ fieldType: f.text() })
|
|
35
|
+
|
|
36
|
+
expect(myField.required).toBe(false)
|
|
37
|
+
expect(myField.unique).toBe(false)
|
|
38
|
+
expect(myField.indexed).toBe(false)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('creates a field with options', () => {
|
|
42
|
+
const myField = field({
|
|
43
|
+
fieldType: f.text(),
|
|
44
|
+
required: true,
|
|
45
|
+
unique: true,
|
|
46
|
+
indexed: true,
|
|
47
|
+
default: 'default value',
|
|
48
|
+
label: 'Name',
|
|
49
|
+
description: 'User name'
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
expect(myField.required).toBe(true)
|
|
53
|
+
expect(myField.unique).toBe(true)
|
|
54
|
+
expect(myField.indexed).toBe(true)
|
|
55
|
+
expect(myField.default).toBe('default value')
|
|
56
|
+
expect(myField.label).toBe('Name')
|
|
57
|
+
expect(myField.description).toBe('User name')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('works with different field types', () => {
|
|
61
|
+
const textField = field({ fieldType: f.text() })
|
|
62
|
+
const numberField = field({ fieldType: f.number() })
|
|
63
|
+
const booleanField = field({ fieldType: f.boolean() })
|
|
64
|
+
const dateField = field({ fieldType: f.date() })
|
|
65
|
+
const timestampField = field({ fieldType: f.timestamp() })
|
|
66
|
+
|
|
67
|
+
expect(textField.fieldType.schema).toBeInstanceOf(z.ZodString)
|
|
68
|
+
expect(numberField.fieldType.schema).toBeInstanceOf(z.ZodNumber)
|
|
69
|
+
expect(booleanField.fieldType.schema).toBeInstanceOf(z.ZodBoolean)
|
|
70
|
+
expect(dateField.fieldType.schema).toBeInstanceOf(z.ZodDate)
|
|
71
|
+
expect(timestampField.fieldType.schema).toBeInstanceOf(z.ZodDate)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('built-in field types', () => {
|
|
76
|
+
it('text field type', () => {
|
|
77
|
+
const instance = f.text()
|
|
78
|
+
expect(instance.schema).toBeInstanceOf(z.ZodString)
|
|
79
|
+
expect(instance.database).toEqual({ type: 'text' })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('email field type', () => {
|
|
83
|
+
const emailField = fieldType({
|
|
84
|
+
schema: z.string().email(),
|
|
85
|
+
database: { type: 'text' }
|
|
86
|
+
})
|
|
87
|
+
const instance = emailField()
|
|
88
|
+
expect(instance.schema).toBeInstanceOf(z.ZodString)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('f.email() creates email field type', () => {
|
|
92
|
+
const instance = f.email()
|
|
93
|
+
expect(instance.schema).toBeInstanceOf(z.ZodString)
|
|
94
|
+
expect(instance.database).toEqual({ type: 'text' })
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('f.url() creates url field type', () => {
|
|
98
|
+
const instance = f.url()
|
|
99
|
+
expect(instance.schema).toBeInstanceOf(z.ZodString)
|
|
100
|
+
expect(instance.database).toEqual({ type: 'text' })
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('number field type', () => {
|
|
104
|
+
const instance = f.number()
|
|
105
|
+
expect(instance.schema).toBeInstanceOf(z.ZodNumber)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('boolean field type', () => {
|
|
109
|
+
const instance = f.boolean()
|
|
110
|
+
expect(instance.schema).toBeInstanceOf(z.ZodBoolean)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('date field type', () => {
|
|
114
|
+
const instance = f.date()
|
|
115
|
+
expect(instance.schema).toBeInstanceOf(z.ZodDate)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('timestamp field type', () => {
|
|
119
|
+
const instance = f.timestamp()
|
|
120
|
+
expect(instance.schema).toBeInstanceOf(z.ZodDate)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('enum field type', () => {
|
|
124
|
+
const status = f.select(['draft', 'published', 'archived'])
|
|
125
|
+
expect(status.schema).toBeInstanceOf(z.ZodEnum)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('json field type', () => {
|
|
129
|
+
const instance = f.json()
|
|
130
|
+
expect(instance.schema).toBeInstanceOf(z.ZodAny)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('array field type', () => {
|
|
134
|
+
const instance = f.array(z.string())
|
|
135
|
+
expect(instance.schema).toBeInstanceOf(z.ZodArray)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('relation field type - one-to-many', () => {
|
|
139
|
+
const instance = f.relation({ collection: 'users' })
|
|
140
|
+
expect(instance.schema).toBeInstanceOf(z.ZodString)
|
|
141
|
+
expect(instance.database).toEqual({
|
|
142
|
+
type: 'integer',
|
|
143
|
+
references: 'users',
|
|
144
|
+
through: undefined,
|
|
145
|
+
many: false,
|
|
146
|
+
singular: false
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('relation field type - one-to-one', () => {
|
|
151
|
+
const instance = f.relation({ collection: 'profiles', singular: true })
|
|
152
|
+
expect(instance.database.singular).toBe(true)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('relation field type - many-to-many', () => {
|
|
156
|
+
const instance = f.relation({ collection: 'tags', many: true })
|
|
157
|
+
expect(instance.schema).toBeInstanceOf(z.ZodArray)
|
|
158
|
+
expect(instance.database.many).toBe(true)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('field with built-in types', () => {
|
|
163
|
+
it('creates a field with text type', () => {
|
|
164
|
+
const myField = field({ fieldType: f.text() })
|
|
165
|
+
expect(myField.fieldType.schema).toBeInstanceOf(z.ZodString)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('creates a field with number type', () => {
|
|
169
|
+
const myField = field({ fieldType: f.number() })
|
|
170
|
+
expect(myField.fieldType.schema).toBeInstanceOf(z.ZodNumber)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('creates a field with relation type', () => {
|
|
174
|
+
const myField = field({
|
|
175
|
+
fieldType: f.relation({ collection: 'users' }),
|
|
176
|
+
required: true
|
|
177
|
+
})
|
|
178
|
+
expect(myField.required).toBe(true)
|
|
179
|
+
expect(myField.fieldType.database.references).toBe('users')
|
|
180
|
+
})
|
|
181
|
+
})
|