@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.
@@ -0,0 +1,2 @@
1
+ export * from './types'
2
+ export * from './collection-operations'
@@ -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
+ })