@geenius/db 0.2.0

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,151 @@
1
+ // @geenius/adapters — Convex DB implementation (MVP tier)
2
+ // Wraps the Convex client to conform to DbAdapter interface.
3
+ // Requires: convex
4
+
5
+ import type { ListOptions, QueryFilter } from '@geenius/adapters-shared'
6
+ import type { DbAdapter } from '@geenius/adapters-shared'
7
+ import { DbError } from '@geenius/adapters-shared'
8
+
9
+ interface ConvexClient {
10
+ query: (fn: unknown, args?: Record<string, unknown>) => Promise<unknown>
11
+ mutation: (fn: unknown, args?: Record<string, unknown>) => Promise<unknown>
12
+ }
13
+
14
+ export interface ConvexDbAdapterOptions {
15
+ /** Pre-configured Convex client */
16
+ client: ConvexClient
17
+ /** Map of collection names to Convex function references */
18
+ functions: {
19
+ [collection: string]: {
20
+ create?: unknown
21
+ get?: unknown
22
+ update?: unknown
23
+ delete?: unknown
24
+ list?: unknown
25
+ query?: unknown
26
+ count?: unknown
27
+ }
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Creates a Convex-backed DbAdapter.
33
+ *
34
+ * Since Convex uses predefined queries/mutations rather than arbitrary SQL,
35
+ * you must provide function references for each collection. The adapter maps
36
+ * generic CRUD calls to these Convex functions.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { api } from '../convex/_generated/api'
41
+ *
42
+ * const db = createConvexDbAdapter({
43
+ * client: convex,
44
+ * functions: {
45
+ * tasks: {
46
+ * create: api.tasks.create,
47
+ * get: api.tasks.get,
48
+ * list: api.tasks.list,
49
+ * update: api.tasks.update,
50
+ * delete: api.tasks.remove,
51
+ * },
52
+ * },
53
+ * })
54
+ * ```
55
+ */
56
+ export function createConvexDbAdapter(options: ConvexDbAdapterOptions): DbAdapter {
57
+ const { client, functions } = options
58
+
59
+ function getFns(collection: string) {
60
+ const fns = functions[collection]
61
+ if (!fns) throw new DbError(`No Convex functions configured for collection: ${collection}`, 'COLLECTION_NOT_CONFIGURED')
62
+ return fns
63
+ }
64
+
65
+ function matchesFilter(item: Record<string, unknown>, filter: QueryFilter): boolean {
66
+ return filter.every((cond) => {
67
+ const val = item[cond.field]
68
+ switch (cond.operator) {
69
+ case 'eq': return val === cond.value
70
+ case 'neq': return val !== cond.value
71
+ case 'gt': return (val as number) > (cond.value as number)
72
+ case 'gte': return (val as number) >= (cond.value as number)
73
+ case 'lt': return (val as number) < (cond.value as number)
74
+ case 'lte': return (val as number) <= (cond.value as number)
75
+ case 'in': return Array.isArray(cond.value) && cond.value.includes(val)
76
+ case 'contains': return typeof val === 'string' && val.includes(cond.value as string)
77
+ default: return false
78
+ }
79
+ })
80
+ }
81
+
82
+ // Convex query/mutation results are loosely typed — the actual shape depends
83
+ // on user-defined Convex functions. We cast at the boundary.
84
+ return {
85
+ async create<T extends Record<string, unknown>>(collection: string, data: Omit<T, 'id'>) {
86
+ const fns = getFns(collection)
87
+ if (!fns.create) throw new DbError(`No create function for ${collection}`, 'COLLECTION_NOT_CONFIGURED')
88
+ const id = await client.mutation(fns.create, data as Record<string, unknown>)
89
+ return { ...data, id } as T & { id: string }
90
+ },
91
+
92
+ async get<T>(collection: string, id: string) {
93
+ const fns = getFns(collection)
94
+ if (!fns.get) throw new DbError(`No get function for ${collection}`, 'COLLECTION_NOT_CONFIGURED')
95
+ const doc = await client.query(fns.get, { id }) as Record<string, unknown> | null
96
+ if (!doc) return null
97
+ return { ...doc, id: (doc._id as string) || id } as T
98
+ },
99
+
100
+ async update<T extends Record<string, unknown>>(collection: string, id: string, data: Partial<T>) {
101
+ const fns = getFns(collection)
102
+ if (!fns.update) throw new DbError(`No update function for ${collection}`, 'COLLECTION_NOT_CONFIGURED')
103
+ await client.mutation(fns.update, { id, ...data } as Record<string, unknown>)
104
+ return this.get<T>(collection, id)
105
+ },
106
+
107
+ async delete(collection: string, id: string) {
108
+ const fns = getFns(collection)
109
+ if (!fns.delete) throw new DbError(`No delete function for ${collection}`, 'COLLECTION_NOT_CONFIGURED')
110
+ try {
111
+ await client.mutation(fns.delete, { id })
112
+ return true
113
+ } catch (err) {
114
+ throw new DbError(`Delete from "${collection}" failed`, 'QUERY_ERROR', err)
115
+ }
116
+ },
117
+
118
+ async list<T>(collection: string, options?: ListOptions) {
119
+ const fns = getFns(collection)
120
+ if (!fns.list) throw new DbError(`No list function for ${collection}`, 'COLLECTION_NOT_CONFIGURED')
121
+ const docs = await client.query(fns.list, {
122
+ limit: options?.limit,
123
+ offset: options?.offset,
124
+ orderBy: options?.orderBy,
125
+ order: options?.order,
126
+ }) as Record<string, unknown>[]
127
+ return (docs || []).map((d) => ({ ...d, id: (d._id as string) || d.id })) as T[]
128
+ },
129
+
130
+ async query<T>(collection: string, filter: QueryFilter) {
131
+ const fns = getFns(collection)
132
+ if (!fns.query) {
133
+ // Fallback: list all and filter client-side
134
+ const all = await this.list<Record<string, unknown>>(collection)
135
+ return all.filter((item) => matchesFilter(item, filter)) as T[]
136
+ }
137
+ const docs = await client.query(fns.query, { filter }) as Record<string, unknown>[]
138
+ return (docs || []).map((d) => ({ ...d, id: (d._id as string) || d.id })) as T[]
139
+ },
140
+
141
+ async count(collection: string, filter?: QueryFilter) {
142
+ const fns = getFns(collection)
143
+ if (fns.count) {
144
+ return client.query(fns.count, filter ? { filter } : undefined) as Promise<number>
145
+ }
146
+ // Fallback: count via list/query
147
+ const items = filter ? await this.query(collection, filter) : await this.list(collection)
148
+ return items.length
149
+ },
150
+ }
151
+ }
@@ -0,0 +1,92 @@
1
+ // @geenius/adapters — InMemory DB implementation
2
+
3
+ import type { ListOptions, QueryFilter, QueryCondition } from '@geenius/adapters-shared'
4
+ import type { DbAdapter } from '@geenius/adapters-shared'
5
+
6
+ function matchesCondition(item: Record<string, unknown>, cond: QueryCondition): boolean {
7
+ const val = item[cond.field]
8
+ switch (cond.operator) {
9
+ case 'eq': return val === cond.value
10
+ case 'neq': return val !== cond.value
11
+ case 'gt': return (val as number) > (cond.value as number)
12
+ case 'gte': return (val as number) >= (cond.value as number)
13
+ case 'lt': return (val as number) < (cond.value as number)
14
+ case 'lte': return (val as number) <= (cond.value as number)
15
+ case 'in': return Array.isArray(cond.value) && cond.value.includes(val)
16
+ case 'contains': return typeof val === 'string' && val.includes(cond.value as string)
17
+ default: return false
18
+ }
19
+ }
20
+
21
+ function matchesFilter(item: Record<string, unknown>, filter: QueryFilter): boolean {
22
+ return filter.every(cond => matchesCondition(item, cond))
23
+ }
24
+
25
+ export function createMemoryDbAdapter(): DbAdapter {
26
+ const store = new Map<string, Record<string, unknown>[]>()
27
+
28
+ function getCollection(collection: string): Record<string, unknown>[] {
29
+ return store.get(collection) || []
30
+ }
31
+
32
+ function saveCollection(collection: string, data: Record<string, unknown>[]) {
33
+ store.set(collection, data)
34
+ }
35
+
36
+ return {
37
+ async create<T extends Record<string, unknown>>(collection: string, data: Omit<T, 'id'>) {
38
+ const items = getCollection(collection)
39
+ const item = { ...data, id: crypto.randomUUID() }
40
+ items.push(item)
41
+ saveCollection(collection, items)
42
+ return item as T & { id: string }
43
+ },
44
+
45
+ async get<T>(collection: string, id: string) {
46
+ const found = getCollection(collection).find(item => item.id === id)
47
+ return (found as T | undefined) ?? null
48
+ },
49
+
50
+ async update<T extends Record<string, unknown>>(collection: string, id: string, data: Partial<T>) {
51
+ const items = getCollection(collection)
52
+ const idx = items.findIndex(item => item.id === id)
53
+ if (idx === -1) return null
54
+ Object.assign(items[idx], data)
55
+ saveCollection(collection, items)
56
+ return items[idx] as T | null
57
+ },
58
+
59
+ async delete(collection: string, id: string) {
60
+ const items = getCollection(collection)
61
+ const idx = items.findIndex(item => item.id === id)
62
+ if (idx === -1) return false
63
+ items.splice(idx, 1)
64
+ saveCollection(collection, items)
65
+ return true
66
+ },
67
+
68
+ async list<T>(collection: string, options?: ListOptions) {
69
+ let items = getCollection(collection)
70
+ if (options?.orderBy) {
71
+ const dir = options.order === 'desc' ? -1 : 1
72
+ items.sort((a, b) => {
73
+ const av = a[options.orderBy!] as string | number, bv = b[options.orderBy!] as string | number
74
+ return av < bv ? -dir : av > bv ? dir : 0
75
+ })
76
+ }
77
+ if (options?.offset) items = items.slice(options.offset)
78
+ if (options?.limit) items = items.slice(0, options.limit)
79
+ return items as T[]
80
+ },
81
+
82
+ async query<T>(collection: string, filter: QueryFilter) {
83
+ return getCollection(collection).filter(item => matchesFilter(item, filter)) as T[]
84
+ },
85
+
86
+ async count(collection: string, filter?: QueryFilter) {
87
+ const items = getCollection(collection)
88
+ if (!filter || filter.length === 0) return items.length
89
+ return items.filter(item => matchesFilter(item, filter)).length
90
+ },
91
+ }
92
+ }
@@ -0,0 +1,137 @@
1
+ // @geenius/adapters — MongoDB implementation (MVP tier)
2
+ // Wraps the MongoDB Node.js driver to conform to DbAdapter interface.
3
+ // Requires: mongodb
4
+
5
+ import type { ListOptions, QueryFilter } from '@geenius/adapters-shared'
6
+ import type { DbAdapter } from '@geenius/adapters-shared'
7
+ import { DbError } from '@geenius/adapters-shared'
8
+
9
+ interface MongoCollection {
10
+ insertOne: (doc: Record<string, unknown>) => Promise<{ insertedId: unknown }>
11
+ findOne: (filter: Record<string, unknown>) => Promise<Record<string, unknown> | null>
12
+ updateOne: (filter: Record<string, unknown>, update: Record<string, unknown>) => Promise<unknown>
13
+ deleteOne: (filter: Record<string, unknown>) => Promise<{ deletedCount: number }>
14
+ find: (filter?: Record<string, unknown>) => MongoCursor
15
+ countDocuments: (filter?: Record<string, unknown>) => Promise<number>
16
+ }
17
+
18
+ interface MongoCursor {
19
+ sort: (sort: Record<string, number>) => MongoCursor
20
+ skip: (n: number) => MongoCursor
21
+ limit: (n: number) => MongoCursor
22
+ toArray: () => Promise<Record<string, unknown>[]>
23
+ }
24
+
25
+ interface MongoDb {
26
+ collection: (name: string) => MongoCollection
27
+ }
28
+
29
+ export interface MongoDbAdapterOptions {
30
+ /** Pre-configured MongoDB database instance */
31
+ db: MongoDb
32
+ /** Optional collection name mapping */
33
+ collectionNames?: Record<string, string>
34
+ }
35
+
36
+ /**
37
+ * Creates a MongoDB-backed DbAdapter.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * import { MongoClient } from 'mongodb'
42
+ *
43
+ * const client = new MongoClient(process.env.MONGODB_URI!)
44
+ * const db = client.db('myapp')
45
+ * const adapter = createMongoDbAdapter({ db })
46
+ * ```
47
+ */
48
+ export function createMongoDbAdapter(options: MongoDbAdapterOptions): DbAdapter {
49
+ const { db, collectionNames = {} } = options
50
+
51
+ function col(collection: string): MongoCollection {
52
+ return db.collection(collectionNames[collection] || collection)
53
+ }
54
+
55
+ function buildMongoFilter(filter: QueryFilter): Record<string, unknown> {
56
+ const mongoFilter: Record<string, unknown> = {}
57
+
58
+ for (const cond of filter) {
59
+ switch (cond.operator) {
60
+ case 'eq': mongoFilter[cond.field] = cond.value; break
61
+ case 'neq': mongoFilter[cond.field] = { $ne: cond.value }; break
62
+ case 'gt': mongoFilter[cond.field] = { $gt: cond.value }; break
63
+ case 'gte': mongoFilter[cond.field] = { $gte: cond.value }; break
64
+ case 'lt': mongoFilter[cond.field] = { $lt: cond.value }; break
65
+ case 'lte': mongoFilter[cond.field] = { $lte: cond.value }; break
66
+ case 'in': mongoFilter[cond.field] = { $in: cond.value }; break
67
+ case 'contains':
68
+ mongoFilter[cond.field] = { $regex: cond.value, $options: 'i' }
69
+ break
70
+ }
71
+ }
72
+
73
+ return mongoFilter
74
+ }
75
+
76
+ function stripMongoId(doc: Record<string, unknown>): Record<string, unknown> {
77
+ const { _id, ...rest } = doc
78
+ return rest
79
+ }
80
+
81
+ return {
82
+ async create<T extends Record<string, unknown>>(collection: string, data: Omit<T, 'id'>) {
83
+ const id = crypto.randomUUID()
84
+ const doc = { ...data, id }
85
+ try {
86
+ await col(collection).insertOne(doc)
87
+ } catch (err) {
88
+ throw new DbError(`Insert into "${collection}" failed`, 'QUERY_ERROR', err)
89
+ }
90
+ return doc as T & { id: string }
91
+ },
92
+
93
+ async get<T>(collection: string, id: string) {
94
+ const doc = await col(collection).findOne({ id })
95
+ if (!doc) return null
96
+ return stripMongoId(doc) as T
97
+ },
98
+
99
+ async update<T extends Record<string, unknown>>(collection: string, id: string, data: Partial<T>) {
100
+ try {
101
+ await col(collection).updateOne({ id }, { $set: data })
102
+ } catch (err) {
103
+ throw new DbError(`Update in "${collection}" failed`, 'QUERY_ERROR', err)
104
+ }
105
+ return this.get<T>(collection, id)
106
+ },
107
+
108
+ async delete(collection: string, id: string) {
109
+ const result = await col(collection).deleteOne({ id })
110
+ return result.deletedCount > 0
111
+ },
112
+
113
+ async list<T>(collection: string, options?: ListOptions) {
114
+ let cursor = col(collection).find()
115
+
116
+ if (options?.orderBy) {
117
+ cursor = cursor.sort({ [options.orderBy]: options.order === 'desc' ? -1 : 1 })
118
+ }
119
+ if (options?.offset) cursor = cursor.skip(options.offset)
120
+ if (options?.limit) cursor = cursor.limit(options.limit)
121
+
122
+ const docs = await cursor.toArray()
123
+ return docs.map(stripMongoId) as T[]
124
+ },
125
+
126
+ async query<T>(collection: string, filter: QueryFilter) {
127
+ const mongoFilter = buildMongoFilter(filter)
128
+ const docs = await col(collection).find(mongoFilter).toArray()
129
+ return docs.map(stripMongoId) as T[]
130
+ },
131
+
132
+ async count(collection: string, filter?: QueryFilter) {
133
+ const mongoFilter = filter ? buildMongoFilter(filter) : {}
134
+ return col(collection).countDocuments(mongoFilter)
135
+ },
136
+ }
137
+ }
@@ -0,0 +1,187 @@
1
+ // @geenius/adapters — Neon/Drizzle DB implementation (MVP tier)
2
+ // Wraps a Drizzle ORM instance over Neon Postgres to conform to DbAdapter.
3
+ // Requires: drizzle-orm, @neondatabase/serverless
4
+ //
5
+ // The adapter accepts pre-built Drizzle operator functions via the options
6
+ // to avoid importing drizzle-orm at build time (it's an optional peer dep).
7
+
8
+ import type { ListOptions, QueryFilter } from '@geenius/adapters-shared'
9
+ import type { DbAdapter } from '@geenius/adapters-shared'
10
+ import { DbError } from '@geenius/adapters-shared'
11
+
12
+ interface DrizzleTable {
13
+ [key: string]: unknown
14
+ }
15
+
16
+ interface DrizzleClient {
17
+ select: (fields?: Record<string, unknown>) => unknown
18
+ insert: (table: unknown) => unknown
19
+ update: (table: unknown) => unknown
20
+ delete: (table: unknown) => unknown
21
+ }
22
+
23
+ /** Drizzle operator functions — injected by the consumer */
24
+ export interface DrizzleOperators {
25
+ eq: (col: unknown, value: unknown) => unknown
26
+ ne: (col: unknown, value: unknown) => unknown
27
+ gt: (col: unknown, value: unknown) => unknown
28
+ gte: (col: unknown, value: unknown) => unknown
29
+ lt: (col: unknown, value: unknown) => unknown
30
+ lte: (col: unknown, value: unknown) => unknown
31
+ inArray: (col: unknown, values: unknown[]) => unknown
32
+ like: (col: unknown, pattern: string) => unknown
33
+ and: (...conditions: unknown[]) => unknown
34
+ asc: (col: unknown) => unknown
35
+ desc: (col: unknown) => unknown
36
+ sql: unknown
37
+ count: (col?: unknown) => unknown
38
+ }
39
+
40
+ export interface NeonDbAdapterOptions {
41
+ /** Pre-configured Drizzle client instance */
42
+ client: DrizzleClient
43
+ /** Map of collection names to Drizzle table schema objects */
44
+ tables: Record<string, DrizzleTable>
45
+ /** Drizzle operator functions (import from 'drizzle-orm') */
46
+ operators: DrizzleOperators
47
+ }
48
+
49
+ /**
50
+ * Creates a Neon-backed DbAdapter using Drizzle ORM.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * import { drizzle } from 'drizzle-orm/neon-http'
55
+ * import { neon } from '@neondatabase/serverless'
56
+ * import { eq, ne, gt, gte, lt, lte, inArray, like, and, asc, desc, sql, count } from 'drizzle-orm'
57
+ * import * as schema from './schema'
58
+ *
59
+ * const db = drizzle(neon(process.env.DATABASE_URL!))
60
+ *
61
+ * const adapter = createNeonDbAdapter({
62
+ * client: db,
63
+ * tables: { tasks: schema.tasks },
64
+ * operators: { eq, ne, gt, gte, lt, lte, inArray, like, and, asc, desc, sql, count },
65
+ * })
66
+ * ```
67
+ */
68
+ export function createNeonDbAdapter(options: NeonDbAdapterOptions): DbAdapter {
69
+ const { client, tables, operators: ops } = options
70
+
71
+ function getTable(collection: string) {
72
+ const table = tables[collection]
73
+ if (!table) throw new DbError(`No table configured for collection: ${collection}`, 'COLLECTION_NOT_CONFIGURED')
74
+ return table
75
+ }
76
+
77
+ function buildConditions(table: DrizzleTable, filter: QueryFilter): unknown[] {
78
+ return filter.map((cond) => {
79
+ const col = table[cond.field]
80
+ if (!col) throw new DbError(`Column ${cond.field} not found in table`, 'QUERY_ERROR')
81
+
82
+ switch (cond.operator) {
83
+ case 'eq': return ops.eq(col, cond.value)
84
+ case 'neq': return ops.ne(col, cond.value)
85
+ case 'gt': return ops.gt(col, cond.value)
86
+ case 'gte': return ops.gte(col, cond.value)
87
+ case 'lt': return ops.lt(col, cond.value)
88
+ case 'lte': return ops.lte(col, cond.value)
89
+ case 'in': return ops.inArray(col, cond.value as unknown[])
90
+ case 'contains': return ops.like(col, `%${cond.value}%`)
91
+ default: throw new DbError(`Unsupported operator: ${cond.operator}`, 'QUERY_ERROR')
92
+ }
93
+ })
94
+ }
95
+
96
+ // Drizzle query builder uses method chaining — typed as `any` at the chain
97
+ // boundary because Drizzle's internal types are complex generics that can't
98
+ // be captured without importing the full drizzle-orm type system.
99
+ /* eslint-disable @typescript-eslint/no-explicit-any */
100
+ return {
101
+ async create<T extends Record<string, unknown>>(collection: string, data: Omit<T, 'id'>) {
102
+ const table = getTable(collection)
103
+ try {
104
+ const [result] = await (client.insert(table) as any).values(data).returning()
105
+ return result as T & { id: string }
106
+ } catch (err) {
107
+ throw new DbError(`Insert into "${collection}" failed`, 'QUERY_ERROR', err)
108
+ }
109
+ },
110
+
111
+ async get<T>(collection: string, id: string) {
112
+ const table = getTable(collection)
113
+ try {
114
+ const [result] = await (client.select() as any).from(table).where(ops.eq(table.id, id)).limit(1)
115
+ return (result as T | undefined) ?? null
116
+ } catch (err) {
117
+ throw new DbError(`Get from "${collection}" failed`, 'QUERY_ERROR', err)
118
+ }
119
+ },
120
+
121
+ async update<T extends Record<string, unknown>>(collection: string, id: string, data: Partial<T>) {
122
+ const table = getTable(collection)
123
+ try {
124
+ const [result] = await (client.update(table) as any).set(data).where(ops.eq(table.id, id)).returning()
125
+ return (result as T | undefined) ?? null
126
+ } catch (err) {
127
+ throw new DbError(`Update in "${collection}" failed`, 'QUERY_ERROR', err)
128
+ }
129
+ },
130
+
131
+ async delete(collection: string, id: string) {
132
+ const table = getTable(collection)
133
+ try {
134
+ const result = await (client.delete(table) as any).where(ops.eq(table.id, id)).returning()
135
+ return Array.isArray(result) && result.length > 0
136
+ } catch (err) {
137
+ throw new DbError(`Delete from "${collection}" failed`, 'QUERY_ERROR', err)
138
+ }
139
+ },
140
+
141
+ async list<T>(collection: string, options?: ListOptions) {
142
+ const table = getTable(collection)
143
+ let query = (client.select() as any).from(table)
144
+
145
+ if (options?.orderBy) {
146
+ const col = table[options.orderBy]
147
+ if (col) {
148
+ query = query.orderBy(options.order === 'desc' ? ops.desc(col) : ops.asc(col))
149
+ }
150
+ }
151
+ if (options?.limit) query = query.limit(options.limit)
152
+ if (options?.offset) query = query.offset(options.offset)
153
+
154
+ return query as T[]
155
+ },
156
+
157
+ async query<T>(collection: string, filter: QueryFilter) {
158
+ const table = getTable(collection)
159
+
160
+ if (filter.length === 0) {
161
+ return (client.select() as any).from(table) as T[]
162
+ }
163
+
164
+ const conditions = buildConditions(table, filter)
165
+
166
+ return (client.select() as any)
167
+ .from(table)
168
+ .where(ops.and(...conditions)) as T[]
169
+ },
170
+
171
+ async count(collection: string, filter?: QueryFilter) {
172
+ const table = getTable(collection)
173
+
174
+ if (!filter || filter.length === 0) {
175
+ const [result] = await (client.select({ count: ops.count() }) as any).from(table)
176
+ return Number(result?.count ?? 0)
177
+ }
178
+
179
+ const conditions = buildConditions(table, filter)
180
+ const [result] = await (client.select({ count: ops.count() }) as any)
181
+ .from(table)
182
+ .where(ops.and(...conditions))
183
+ return Number(result?.count ?? 0)
184
+ },
185
+ }
186
+ /* eslint-enable @typescript-eslint/no-explicit-any */
187
+ }