@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 ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@deessejs/collections",
3
+ "version": "0.0.1",
4
+ "description": "High-level abstraction layer built on top of Drizzle ORM",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./fields": {
15
+ "types": "./dist/fields/index.d.ts",
16
+ "import": "./dist/fields/index.mjs",
17
+ "require": "./dist/fields/index.js"
18
+ },
19
+ "./plugins": {
20
+ "types": "./dist/plugins/index.d.ts",
21
+ "import": "./dist/plugins/index.mjs",
22
+ "require": "./dist/plugins/index.js"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "dev": "tsup --watch",
28
+ "lint": "eslint src/",
29
+ "typecheck": "tsc --noEmit",
30
+ "test": "vitest run",
31
+ "test:integration": "vitest run --config vitest.integration.config.ts",
32
+ "test:watch": "vitest",
33
+ "test:coverage": "vitest run --coverage"
34
+ },
35
+ "dependencies": {
36
+ "drizzle-kit": "^0.22.0",
37
+ "drizzle-orm": "^0.31.2",
38
+ "pg": "^8.11.0",
39
+ "zod": "^3.23.8"
40
+ },
41
+ "devDependencies": {
42
+ "@deessejs/type-testing": "^0.1.5",
43
+ "@neondatabase/serverless": "^1.0.2",
44
+ "@types/node": "^20.14.12",
45
+ "@types/pg": "^8.11.0",
46
+ "eslint": "^8.57.0",
47
+ "neon-serverless": "^0.5.3",
48
+ "tsup": "^8.2.2",
49
+ "tsx": "^4.21.0",
50
+ "typescript": "^5.5.4",
51
+ "vitest": "^2.0.5"
52
+ },
53
+ "peerDependencies": {
54
+ "typescript": ">=5"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public",
58
+ "registry": "https://registry.npmjs.org/"
59
+ }
60
+ }
package/src/adapter.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * PostgreSQL adapter configuration
3
+ */
4
+ export type PgAdapterConfig = {
5
+ url: string
6
+ migrationsPath?: string
7
+ }
8
+
9
+ /**
10
+ * PostgreSQL adapter
11
+ */
12
+ export interface PgAdapter {
13
+ type: 'postgres'
14
+ config: PgAdapterConfig
15
+ }
16
+
17
+ /**
18
+ * Database adapter type
19
+ */
20
+ export type DatabaseAdapter = PgAdapter
21
+
22
+ /**
23
+ * Creates a PostgreSQL adapter
24
+ *
25
+ * @example
26
+ * const adapter = pgAdapter({
27
+ * url: 'postgres://user:pass@localhost:5432/db'
28
+ * })
29
+ */
30
+ export const pgAdapter = (config: PgAdapterConfig): PgAdapter => {
31
+ return {
32
+ type: 'postgres',
33
+ config: {
34
+ url: config.url,
35
+ migrationsPath: config.migrationsPath ?? './migrations'
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,138 @@
1
+ import type { FieldDefinition } from './field'
2
+
3
+ /**
4
+ * Collection configuration
5
+ */
6
+ export type CollectionConfig<T extends Record<string, unknown> = Record<string, unknown>> = {
7
+ slug: string
8
+ name?: string
9
+ fields: Record<string, FieldDefinition>
10
+ hooks?: CollectionHooks
11
+ dataType?: T
12
+ }
13
+
14
+ /**
15
+ * Operation types
16
+ */
17
+ export type OperationType = 'create' | 'update' | 'delete' | 'read'
18
+
19
+ /**
20
+ * Hook context base
21
+ */
22
+ export type HookContextBase = {
23
+ collection: string
24
+ operation: OperationType
25
+ }
26
+
27
+ /**
28
+ * Before/After Operation context
29
+ */
30
+ export type OperationHookContext = HookContextBase & {
31
+ data?: Record<string, unknown>
32
+ where?: Record<string, unknown>
33
+ result?: unknown
34
+ }
35
+
36
+ /**
37
+ * Before/After Create context
38
+ */
39
+ export type CreateHookContext = HookContextBase & {
40
+ operation: 'create'
41
+ data: Record<string, unknown>
42
+ result?: unknown
43
+ db?: unknown
44
+ }
45
+
46
+ /**
47
+ * Before/After Update context
48
+ */
49
+ export type UpdateHookContext = HookContextBase & {
50
+ operation: 'update'
51
+ data: Record<string, unknown>
52
+ where: Record<string, unknown>
53
+ previousData?: Record<string, unknown>
54
+ result?: unknown
55
+ db?: unknown
56
+ }
57
+
58
+ /**
59
+ * Before/After Delete context
60
+ */
61
+ export type DeleteHookContext = HookContextBase & {
62
+ operation: 'delete'
63
+ where: Record<string, unknown>
64
+ previousData?: Record<string, unknown>
65
+ result?: unknown
66
+ db?: unknown
67
+ }
68
+
69
+ /**
70
+ * Before/After Read context
71
+ */
72
+ export type ReadHookContext = HookContextBase & {
73
+ operation: 'read'
74
+ query?: Record<string, unknown>
75
+ result?: unknown[]
76
+ db?: unknown
77
+ }
78
+
79
+ /**
80
+ * Generic hook function type
81
+ */
82
+ export type GenericHookFunction = (context: OperationHookContext) => Promise<void> | void
83
+ export type CreateHookFunction = (context: CreateHookContext) => Promise<void> | void
84
+ export type UpdateHookFunction = (context: UpdateHookContext) => Promise<void> | void
85
+ export type DeleteHookFunction = (context: DeleteHookContext) => Promise<void> | void
86
+ export type ReadHookFunction = (context: ReadHookContext) => Promise<void> | void
87
+
88
+ /**
89
+ * Collection hooks
90
+ */
91
+ export type CollectionHooks = {
92
+ beforeOperation?: GenericHookFunction[]
93
+ afterOperation?: GenericHookFunction[]
94
+ beforeCreate?: CreateHookFunction[]
95
+ afterCreate?: CreateHookFunction[]
96
+ beforeUpdate?: UpdateHookFunction[]
97
+ afterUpdate?: UpdateHookFunction[]
98
+ beforeDelete?: DeleteHookFunction[]
99
+ afterDelete?: DeleteHookFunction[]
100
+ beforeRead?: ReadHookFunction[]
101
+ afterRead?: ReadHookFunction[]
102
+ }
103
+
104
+ /**
105
+ * A collection definition
106
+ */
107
+ export type Collection<T extends Record<string, unknown> = Record<string, unknown>> = {
108
+ slug: string
109
+ name?: string
110
+ fields: Record<string, FieldDefinition>
111
+ hooks?: CollectionHooks
112
+ dataType?: T
113
+ }
114
+
115
+ /**
116
+ * Creates a new collection
117
+ *
118
+ * @example
119
+ * export const users = collection({
120
+ * slug: 'users',
121
+ * name: 'Users',
122
+ * fields: {
123
+ * name: field({ fieldType: text }),
124
+ * email: field({ fieldType: email, unique: true })
125
+ * }
126
+ * })
127
+ */
128
+ export const collection = <T extends Record<string, unknown> = Record<string, unknown>>(
129
+ config: CollectionConfig<T>
130
+ ): Collection<T> => {
131
+ return {
132
+ slug: config.slug,
133
+ name: config.name,
134
+ fields: config.fields,
135
+ hooks: config.hooks,
136
+ dataType: config.dataType
137
+ }
138
+ }
package/src/config.ts ADDED
@@ -0,0 +1,134 @@
1
+ import { Pool, type Pool as PoolType } from 'pg'
2
+ import { drizzle } from 'drizzle-orm/node-postgres'
3
+
4
+ import type { Collection } from './collection'
5
+ import type { DatabaseAdapter } from './adapter'
6
+ import { buildSchema } from './schema'
7
+
8
+ /**
9
+ * Plugin interface
10
+ */
11
+ export type Plugin = {
12
+ name: string
13
+ collections?: Record<string, Collection>
14
+ hooks?: Record<string, unknown[]>
15
+ }
16
+
17
+ /**
18
+ * Configuration options
19
+ */
20
+ export type ConfigOptions<T extends Collection[] = []> = {
21
+ database: DatabaseAdapter
22
+ collections: T
23
+ plugins?: Plugin[]
24
+ }
25
+
26
+ /**
27
+ * Define config return type with inferred collection keys
28
+ *
29
+ * - collections: metadata only (slug, name, fields, hooks, dataType)
30
+ * - db: Drizzle instance with operations (via schema tables)
31
+ * - $meta: array of collection slugs and plugin names
32
+ */
33
+ export type DefineConfigReturn<T extends Collection[] = []> = {
34
+ collections: {
35
+ [K in T[number] as K['slug']]: Collection
36
+ }
37
+ db: ReturnType<typeof drizzle<Record<string, unknown>>>
38
+ $meta: {
39
+ collections: T[number]['slug'][]
40
+ plugins: string[]
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Creates the configuration for the data layer
46
+ *
47
+ * @example
48
+ * const adapter = pgAdapter({
49
+ * url: process.env.DATABASE_URL!
50
+ * })
51
+ *
52
+ * export const { collections, db } = defineConfig({
53
+ * database: adapter,
54
+ * collections: [users, posts],
55
+ * plugins: [timestampsPlugin()]
56
+ * })
57
+ *
58
+ * // collections: metadata only
59
+ * collections.users.slug // 'users'
60
+ * collections.users.fields // { name, email, ... }
61
+ *
62
+ * // db: Drizzle instance with operations
63
+ * await db.users.findMany()
64
+ * await db.users.insert(values)
65
+ */
66
+ export const defineConfig = <T extends Collection[]>(
67
+ options: ConfigOptions<T>
68
+ ): DefineConfigReturn<T> => {
69
+ // Initialize the database connection based on adapter type
70
+ let pool: PoolType | null = null
71
+ let dbInstance: ReturnType<typeof drizzle<Record<string, unknown>>> | null = null
72
+
73
+ let schema: Record<string, unknown> = {}
74
+
75
+ if (options.database.type === 'postgres') {
76
+ // Create pool from adapter config
77
+ pool = new Pool({
78
+ connectionString: options.database.config.url
79
+ })
80
+
81
+ // Build schema from collections
82
+ schema = buildSchema(options.collections as Collection[])
83
+
84
+ // Create Drizzle instance with schema
85
+ dbInstance = drizzle(pool, { schema })
86
+ }
87
+
88
+ // Build collections map (metadata only)
89
+ const collectionsMap: Record<string, Collection> = {}
90
+ const collectionNames: string[] = []
91
+
92
+ for (const coll of options.collections) {
93
+ // Store only metadata (not operations)
94
+ collectionsMap[coll.slug] = {
95
+ slug: coll.slug,
96
+ name: coll.name,
97
+ fields: coll.fields,
98
+ hooks: coll.hooks,
99
+ dataType: coll.dataType
100
+ }
101
+ collectionNames.push(coll.slug)
102
+ }
103
+
104
+ // Build plugins map
105
+ const pluginNames: string[] = []
106
+ if (options.plugins) {
107
+ for (const plugin of options.plugins) {
108
+ pluginNames.push(plugin.name)
109
+
110
+ // Register plugin collections (metadata only)
111
+ if (plugin.collections) {
112
+ for (const [name, coll] of Object.entries(plugin.collections)) {
113
+ collectionsMap[name] = {
114
+ slug: coll.slug,
115
+ name: coll.name,
116
+ fields: coll.fields,
117
+ hooks: coll.hooks,
118
+ dataType: coll.dataType
119
+ }
120
+ collectionNames.push(name)
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ return {
127
+ collections: collectionsMap as DefineConfigReturn<T>['collections'],
128
+ db: dbInstance as DefineConfigReturn<T>['db'],
129
+ $meta: {
130
+ collections: collectionNames as DefineConfigReturn<T>['$meta']['collections'],
131
+ plugins: pluginNames
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * A field type instance (already configured)
5
+ */
6
+ export type FieldTypeInstance = {
7
+ schema: z.ZodType
8
+ database: unknown
9
+ }
10
+
11
+ /**
12
+ * A field type creator (needs to be called to get instance)
13
+ */
14
+ export type FieldTypeCreator = () => FieldTypeInstance
15
+
16
+ /**
17
+ * Field type configuration
18
+ */
19
+ export type FieldTypeConfig = {
20
+ schema: z.ZodType
21
+ database?: unknown
22
+ }
23
+
24
+ /**
25
+ * Creates a new field type
26
+ *
27
+ * @example
28
+ * const text = fieldType({
29
+ * schema: z.string(),
30
+ * database: { type: 'text' }
31
+ * })
32
+ *
33
+ * const textField = text() // Get instance
34
+ */
35
+ export const fieldType = (config: FieldTypeConfig): (() => FieldTypeInstance) => {
36
+ return () => ({
37
+ schema: config.schema,
38
+ database: config.database ?? {}
39
+ })
40
+ }
package/src/field.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { FieldTypeInstance } from './field-type'
2
+
3
+ /**
4
+ * Field configuration options
5
+ */
6
+ export type FieldOptions = {
7
+ fieldType: FieldTypeInstance
8
+ required?: boolean
9
+ unique?: boolean
10
+ indexed?: boolean
11
+ default?: unknown
12
+ label?: string
13
+ description?: string
14
+ }
15
+
16
+ /**
17
+ * Creates a field definition
18
+ *
19
+ * @example
20
+ * name: field({ fieldType: text() })
21
+ * email: field({ fieldType: email(), unique: true })
22
+ */
23
+ export const field = (config: FieldOptions): FieldDefinition => {
24
+ return {
25
+ fieldType: config.fieldType,
26
+ required: config.required ?? false,
27
+ unique: config.unique ?? false,
28
+ indexed: config.indexed ?? false,
29
+ default: config.default,
30
+ label: config.label,
31
+ description: config.description
32
+ }
33
+ }
34
+
35
+ /**
36
+ * A field definition
37
+ */
38
+ export type FieldDefinition = {
39
+ fieldType: FieldTypeInstance
40
+ required: boolean
41
+ unique: boolean
42
+ indexed: boolean
43
+ default?: unknown
44
+ label?: string
45
+ description?: string
46
+ }
@@ -0,0 +1,192 @@
1
+ import { fieldType, type FieldTypeInstance } from '../field-type'
2
+ import { z } from 'zod'
3
+
4
+ /**
5
+ * Derive the database type string from a Zod schema
6
+ */
7
+ const getItemType = (itemSchema: z.ZodType): string => {
8
+ if (itemSchema instanceof z.ZodString) return 'text'
9
+ if (itemSchema instanceof z.ZodNumber) return 'integer'
10
+ if (itemSchema instanceof z.ZodBoolean) return 'boolean'
11
+ if (itemSchema instanceof z.ZodDate) return 'timestamp'
12
+ if (itemSchema instanceof z.ZodEnum) return 'text'
13
+ if (itemSchema instanceof z.ZodArray) return 'array'
14
+ if (itemSchema instanceof z.ZodObject) return 'jsonb'
15
+ return 'text'
16
+ }
17
+
18
+ /**
19
+ * Field types namespace (like zod's z)
20
+ */
21
+ export const f = {
22
+ /**
23
+ * Text field type
24
+ */
25
+ text: (): FieldTypeInstance => fieldType({
26
+ schema: z.string(),
27
+ database: { type: 'text' }
28
+ })(),
29
+
30
+ /**
31
+ * Email field type with built-in validation
32
+ */
33
+ email: (): FieldTypeInstance => fieldType({
34
+ schema: z.string().email(),
35
+ database: { type: 'text' }
36
+ })(),
37
+
38
+ /**
39
+ * URL field type with built-in validation
40
+ */
41
+ url: (): FieldTypeInstance => fieldType({
42
+ schema: z.string().url(),
43
+ database: { type: 'text' }
44
+ })(),
45
+
46
+ /**
47
+ * Number field type
48
+ */
49
+ number: (): FieldTypeInstance => fieldType({
50
+ schema: z.number(),
51
+ database: { type: 'integer' }
52
+ })(),
53
+
54
+ /**
55
+ * Boolean field type
56
+ */
57
+ boolean: (): FieldTypeInstance => fieldType({
58
+ schema: z.boolean(),
59
+ database: { type: 'boolean' }
60
+ })(),
61
+
62
+ /**
63
+ * Date field type (date only, no time)
64
+ */
65
+ date: (): FieldTypeInstance => fieldType({
66
+ schema: z.date(),
67
+ database: { type: 'date' }
68
+ })(),
69
+
70
+ /**
71
+ * Timestamp field type (date with time)
72
+ */
73
+ timestamp: (): FieldTypeInstance => fieldType({
74
+ schema: z.date(),
75
+ database: { type: 'timestamp' }
76
+ })(),
77
+
78
+ /**
79
+ * Creates a select field type
80
+ */
81
+ select: <T extends readonly [string, ...string[]]>(
82
+ options: T
83
+ ): FieldTypeInstance => fieldType({
84
+ schema: z.enum(options),
85
+ database: { type: 'text' }
86
+ })(),
87
+
88
+ /**
89
+ * JSON field type for storing JSON data
90
+ */
91
+ json: (schema?: z.ZodType): FieldTypeInstance => fieldType({
92
+ schema: schema ?? z.any(),
93
+ database: { type: 'jsonb' }
94
+ })(),
95
+
96
+ /**
97
+ * Array field type for storing lists
98
+ */
99
+ array: (itemSchema: z.ZodType): FieldTypeInstance => fieldType({
100
+ schema: z.array(itemSchema),
101
+ database: { type: 'array', itemType: getItemType(itemSchema) }
102
+ })(),
103
+
104
+ /**
105
+ * Creates a relation field type for foreign key relationships
106
+ */
107
+ relation: (options: {
108
+ collection: string
109
+ singular?: boolean
110
+ many?: boolean
111
+ through?: string
112
+ }): FieldTypeInstance => {
113
+ const isMany = options.many ?? false
114
+ const isSingular = options.singular ?? false
115
+
116
+ return fieldType({
117
+ schema: isMany ? z.array(z.string()) : z.string(),
118
+ database: {
119
+ type: 'integer',
120
+ references: options.collection,
121
+ through: options.through,
122
+ many: isMany,
123
+ singular: isSingular
124
+ }
125
+ })()
126
+ }
127
+ }
128
+
129
+ /**
130
+ * @deprecated Use f.text() instead
131
+ */
132
+ export const text = f.text
133
+
134
+ /**
135
+ * @deprecated Use f.email() instead
136
+ */
137
+ export const email = f.email
138
+
139
+ /**
140
+ * @deprecated Use f.url() instead
141
+ */
142
+ export const url = f.url
143
+
144
+ /**
145
+ * @deprecated Use f.number() instead
146
+ */
147
+ export const number = f.number
148
+
149
+ /**
150
+ * @deprecated Use f.boolean() instead
151
+ */
152
+ export const boolean = f.boolean
153
+
154
+ /**
155
+ * @deprecated Use f.date() instead
156
+ */
157
+ export const date = f.date
158
+
159
+ /**
160
+ * @deprecated Use f.timestamp() instead
161
+ */
162
+ export const timestamp = f.timestamp
163
+
164
+ /**
165
+ * @deprecated Use f.select() instead
166
+ */
167
+ export const select = f.select
168
+
169
+ /**
170
+ * @deprecated Use f.json() instead
171
+ */
172
+ export const json = f.json
173
+
174
+ /**
175
+ * @deprecated Use f.array() instead
176
+ */
177
+ export const array = f.array
178
+
179
+ /**
180
+ * @deprecated Use f.relation() instead
181
+ */
182
+ export const relation = f.relation
183
+
184
+ /**
185
+ * @deprecated Use f.relation instead
186
+ */
187
+ export type RelationOptions = {
188
+ collection: string
189
+ singular?: boolean
190
+ many?: boolean
191
+ through?: string
192
+ }
@@ -0,0 +1 @@
1
+ export * from './f'
package/src/index.ts ADDED
@@ -0,0 +1,27 @@
1
+ // Field type system
2
+ export { fieldType, type FieldTypeInstance, type FieldTypeCreator, type FieldTypeConfig } from './field-type'
3
+
4
+ // Field
5
+ export { field, type FieldDefinition, type FieldOptions } from './field'
6
+
7
+ // Built-in field types
8
+ export { f } from './fields'
9
+ export * from './fields'
10
+
11
+ // Collection
12
+ export { collection, type CollectionConfig, type Collection, type CollectionHooks, type OperationType, type CreateHookContext, type UpdateHookContext, type DeleteHookContext, type ReadHookContext, type OperationHookContext, type CreateHookFunction, type UpdateHookFunction, type DeleteHookFunction, type ReadHookFunction, type GenericHookFunction } from './collection'
13
+
14
+ // Operations
15
+ export * from './operations'
16
+
17
+ // Adapter
18
+ export { pgAdapter, type PgAdapter, type PgAdapterConfig, type DatabaseAdapter } from './adapter'
19
+
20
+ // Schema
21
+ export { buildSchema, buildTable } from './schema'
22
+
23
+ // Migrations
24
+ export { push, generate, migrate } from './migrations'
25
+
26
+ // Config
27
+ export { defineConfig, type Plugin, type ConfigOptions, type DefineConfigReturn } from './config'