@ragestudio/scylla-odm 0.1.0 → 0.3.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.
@@ -1,90 +0,0 @@
1
- import type { Model } from "../model"
2
- import typeChecker from "../utils/typeChecker"
3
-
4
- export class Result<TDoc = any> {
5
- constructor(data: TDoc, model: Model<TDoc>) {
6
- if (data == null) {
7
- throw new Error("Cannot create Result with null or undefined data")
8
- }
9
-
10
- if (typeof data !== "object" || Array.isArray(data)) {
11
- throw new Error("Result data must be an object")
12
- }
13
-
14
- Object.assign(this, data)
15
-
16
- Object.defineProperty(this, "_model", {
17
- value: model,
18
- enumerable: false,
19
- writable: false,
20
- configurable: false,
21
- })
22
- }
23
-
24
- _model: Model<TDoc>
25
-
26
- async save() {
27
- try {
28
- const data = this.toRaw()
29
-
30
- typeChecker(this._model, data)
31
-
32
- return await this._model.update(data as any)
33
- } catch (error: any) {
34
- throw new Error(`Failed to save result: ${error.message}`)
35
- }
36
- }
37
-
38
- async delete() {
39
- try {
40
- return await this._model.delete(this.toRaw() as any)
41
- } catch (error: any) {
42
- throw new Error(`Failed to delete result: ${error.message}`)
43
- }
44
- }
45
-
46
- toRaw(): TDoc {
47
- const raw: any = {}
48
-
49
- for (const key in this) {
50
- if (key === "_model") continue
51
-
52
- if (this.propertyIsEnumerable(key)) {
53
- const value = (this as any)[key]
54
-
55
- try {
56
- JSON.stringify(value)
57
- raw[key] = value
58
- } catch (error) {
59
- raw[key] = String(value)
60
- }
61
- }
62
- }
63
-
64
- return raw as TDoc
65
- }
66
-
67
- isValid(): boolean {
68
- try {
69
- typeChecker(this._model, this.toRaw())
70
- return true
71
- } catch {
72
- return false
73
- }
74
- }
75
-
76
- getChangedFields(original: Partial<TDoc>): (keyof TDoc)[] {
77
- const current = this.toRaw() as Record<string, any>
78
- const changed: (keyof TDoc)[] = []
79
-
80
- for (const key in current) {
81
- if (!(key in original) || current[key] !== (original as any)[key]) {
82
- changed.push(key as keyof TDoc)
83
- }
84
- }
85
-
86
- return changed
87
- }
88
- }
89
-
90
- export default Result
@@ -1,16 +0,0 @@
1
- import { TableKeys } from "../types"
2
-
3
- export class Schema<T> {
4
- public readonly table_name: string
5
- public readonly clustering_order: any
6
- public readonly keys: TableKeys
7
- public readonly fields: T
8
-
9
- constructor(params: any, fields: T) {
10
- this.table_name = params.table_name
11
- this.keys = params.keys
12
- this.fields = fields
13
- }
14
- }
15
-
16
- export default Schema
package/src/types.ts DELETED
@@ -1,89 +0,0 @@
1
- import Result from "./result"
2
- import { Schema } from "./schema"
3
-
4
- export type ClientConfig = {
5
- modelsPath?: string
6
- contactPoints?: string[]
7
- localDataCenter?: string
8
- keyspace?: string
9
- port?: number
10
- maxRetries?: number
11
- retryDelay?: number
12
- pooling?: {
13
- coreConnectionsPerHost?: Record<string, number>
14
- maxRequestsPerConnection?: number
15
- }
16
- }
17
-
18
- export enum ColumnTypes {
19
- Ascii = "ascii",
20
- Bigint = "bigint",
21
- Blob = "blob",
22
- Boolean = "boolean",
23
- Counter = "counter",
24
- Date = "date",
25
- Decimal = "decimal",
26
- Double = "double",
27
- Duration = "duration",
28
- Float = "float",
29
- Frozen = "frozen",
30
- Inet = "inet",
31
- Int = "int",
32
- List = "list",
33
- Map = "map",
34
- Set = "set",
35
- Smallint = "smallint",
36
- Text = "text",
37
- Time = "time",
38
- Timestamp = "timestamp",
39
- Timeuuid = "timeuuid",
40
- Tinyint = "tinyint",
41
- Tuple = "tuple",
42
- Uuid = "uuid",
43
- Varchar = "varchar",
44
- Varint = "varint",
45
- }
46
-
47
- export type QueryOperators<TValue> = {
48
- $eq?: TValue
49
- $ne?: TValue
50
- $in?: TValue[]
51
- $gt?: TValue
52
- $gte?: TValue
53
- $lt?: TValue
54
- $lte?: TValue
55
- }
56
-
57
- export type TableKeys = (string | TableKeys)[]
58
-
59
- export interface Column<T> {
60
- type?: ColumnTypes | string
61
- required?: boolean
62
- }
63
-
64
- export type InferRawData<T> = {
65
- [K in keyof T as K extends `$${string}` ? never : K]: T[K] extends Column<
66
- infer U
67
- >
68
- ? U
69
- : T[K]
70
- }
71
-
72
- export type InferDocument<S> =
73
- S extends Schema<infer T> ? InferRawData<T> : never
74
-
75
- export type DocumentResult<TDoc> = Result<TDoc> & TDoc
76
-
77
- export type QueryOptions = {
78
- raw?: boolean
79
- }
80
-
81
- export type OrderBy<TDoc> = { [K in keyof TDoc]?: "asc" | "desc" }
82
-
83
- export type Query<TDoc> = {
84
- [K in keyof TDoc]?: TDoc[K] | QueryOperators<TDoc[K]>
85
- } & {
86
- $and?: Query<TDoc>[]
87
- $limit?: number
88
- $orderby?: OrderBy<TDoc>
89
- }
@@ -1,10 +0,0 @@
1
- export default (map) => {
2
- return map.reduce((obj, { name, schema }) => {
3
- return {
4
- ...obj,
5
- [name]: {
6
- tables: [schema.table_name],
7
- },
8
- }
9
- }, {})
10
- }
@@ -1,30 +0,0 @@
1
- export default function fillDefaults(schema: any, data: any) {
2
- const defaults = schema.options?.defaults
3
-
4
- if (!defaults || Object.keys(defaults).length === 0) {
5
- return data
6
- }
7
-
8
- let needsDefaults = false
9
-
10
- for (const key in defaults) {
11
- if (data[key] == null) {
12
- needsDefaults = true
13
- break
14
- }
15
- }
16
-
17
- if (!needsDefaults) {
18
- return data
19
- }
20
-
21
- const result = Object.assign({}, data)
22
-
23
- for (const key in defaults) {
24
- if (result[key] == null) {
25
- result[key] = defaults[key]
26
- }
27
- }
28
-
29
- return result
30
- }
@@ -1,39 +0,0 @@
1
- import fs from "node:fs"
2
- import path from "node:path"
3
-
4
- export default async (fromPath: string): Promise<any[]> => {
5
- if (typeof fromPath !== "string") {
6
- return []
7
- }
8
-
9
- if (!fs.existsSync(fromPath)) {
10
- console.warn(
11
- `Cannot load models from [${fromPath}] case this path does not exist`,
12
- )
13
- return []
14
- }
15
-
16
- let schemas = []
17
-
18
- let files = await fs.promises.readdir(fromPath)
19
-
20
- files = files.filter((file) => file.endsWith(".js") || file.endsWith(".ts"))
21
-
22
- for await (const file of files) {
23
- const name = file.replace(".js", "")
24
- const file_path = path.join(fromPath, file)
25
-
26
- try {
27
- let mod = await import(file_path)
28
-
29
- mod = mod.default
30
-
31
- schemas.push(mod)
32
- } catch (error) {
33
- console.error(`Failed to load schema [${name}]:`, error)
34
- continue
35
- }
36
- }
37
-
38
- return schemas
39
- }
@@ -1,192 +0,0 @@
1
- import type { Model } from "../model"
2
-
3
- // @ts-ignore
4
- import cassandra from "cassandra-driver"
5
- const { q } = cassandra.mapping
6
-
7
- const MAX_QUERY_DEPTH = 3
8
- const MAX_IN_ELEMENTS = 1000
9
-
10
- const VALID_OPERATORS = new Set([
11
- "$eq",
12
- "$ne",
13
- "$gt",
14
- "$gte",
15
- "$lt",
16
- "$lte",
17
- "$in",
18
- ])
19
-
20
- function requireNotNull(value: any, operator: string): void {
21
- if (value === null || value === undefined) {
22
- throw new Error(
23
- `${operator} operator cannot compare with null or undefined`,
24
- )
25
- }
26
- }
27
-
28
- function buildOperator(operator: string, opValue: any): any {
29
- if (!VALID_OPERATORS.has(operator)) {
30
- throw new Error(`Invalid operator: ${operator}`)
31
- }
32
-
33
- switch (operator) {
34
- case "$eq":
35
- return opValue
36
-
37
- case "$ne":
38
- requireNotNull(opValue, "$ne")
39
- return q.notEq(opValue)
40
-
41
- case "$in":
42
- if (!Array.isArray(opValue)) {
43
- throw new Error("$in operator requires an array")
44
- }
45
- if (opValue.length > MAX_IN_ELEMENTS) {
46
- throw new Error(
47
- `$in operator exceeds maximum of ${MAX_IN_ELEMENTS} elements`,
48
- )
49
- }
50
- for (let i = 0; i < opValue.length; i++) {
51
- if (opValue[i] === null || opValue[i] === undefined) {
52
- throw new Error(
53
- `$in array element at index ${i} cannot be null or undefined`,
54
- )
55
- }
56
- }
57
- return q.in_(opValue)
58
-
59
- case "$gt":
60
- requireNotNull(opValue, "$gt")
61
- return q.gt(opValue)
62
-
63
- case "$gte":
64
- requireNotNull(opValue, "$gte")
65
- return q.gte(opValue)
66
-
67
- case "$lt":
68
- requireNotNull(opValue, "$lt")
69
- return q.lt(opValue)
70
-
71
- case "$lte":
72
- requireNotNull(opValue, "$lte")
73
- return q.lte(opValue)
74
- }
75
- }
76
-
77
- export default function queryParser(
78
- model: Model<any>,
79
- query: any,
80
- depth: number = 0,
81
- ) {
82
- if (depth > MAX_QUERY_DEPTH) {
83
- throw new Error(`Query depth exceeds maximum of ${MAX_QUERY_DEPTH}`)
84
- }
85
-
86
- if (!query || typeof query !== "object") {
87
- return query
88
- }
89
-
90
- const parsedQuery: Record<string, any> = {}
91
- const fields = model.schema.fields
92
-
93
- for (const field of Object.keys(query)) {
94
- const value = query[field]
95
-
96
- if (field === "$and") {
97
- handleAnd(model, value, parsedQuery, depth)
98
- continue
99
- }
100
-
101
- if (field === "$or") {
102
- throw new Error(
103
- "ScyllaDB does not support OR queries across different columns. Use $in for a single column.",
104
- )
105
- }
106
-
107
- if (!isValidFieldName(fields, field)) {
108
- throw new Error(
109
- `Invalid field name: [${field}] or it does not exist in schema`,
110
- )
111
- }
112
-
113
- parsedQuery[field] = parseField(value)
114
- }
115
-
116
- return parsedQuery
117
- }
118
-
119
- function handleAnd(
120
- model: Model<any>,
121
- conditions: any,
122
- parsedQuery: Record<string, any>,
123
- depth: number,
124
- ) {
125
- if (!Array.isArray(conditions)) {
126
- throw new Error("$and operator requires an array")
127
- }
128
- if (conditions.length > 10) {
129
- throw new Error("$and operator exceeds maximum of 10 conditions")
130
- }
131
-
132
- for (let i = 0; i < conditions.length; i++) {
133
- const condition = conditions[i]
134
- if (!condition || typeof condition !== "object") {
135
- throw new Error(`$and condition at index ${i} must be an object`)
136
- }
137
-
138
- const parsed = queryParser(model, condition, depth + 1)
139
- for (const key of Object.keys(parsed)) {
140
- if (key in parsedQuery) {
141
- throw new Error(
142
- `$and conflict: field "${key}" appears in multiple conditions`,
143
- )
144
- }
145
- }
146
- Object.assign(parsedQuery, parsed)
147
- }
148
- }
149
-
150
- function parseField(value: any): any {
151
- if (
152
- value === null ||
153
- typeof value !== "object" ||
154
- Array.isArray(value) ||
155
- value instanceof Date
156
- ) {
157
- if (Array.isArray(value)) {
158
- throw new Error(
159
- "Array values require explicit operator (e.g., $in)",
160
- )
161
- }
162
- return value
163
- }
164
-
165
- const operators = Object.keys(value)
166
- const compiledOps = operators.map((op) => buildOperator(op, value[op]))
167
-
168
- return compiledOps.length === 1
169
- ? compiledOps[0]
170
- : (q.and as any)(...compiledOps)
171
- }
172
-
173
- export function isValidFieldName(
174
- fields: Record<string, any>,
175
- fieldName: string,
176
- ): boolean {
177
- const invalidPatterns = [
178
- /^[0-9]/,
179
- /[^a-zA-Z0-9_]/,
180
- /^(select|insert|update|delete|drop|create|alter|truncate)$/i,
181
- ]
182
-
183
- for (const pattern of invalidPatterns) {
184
- if (pattern.test(fieldName)) return false
185
- }
186
-
187
- return fieldName in fields
188
- }
189
-
190
- export function isValidOperator(operator: string): boolean {
191
- return VALID_OPERATORS.has(operator)
192
- }
@@ -1,100 +0,0 @@
1
- // @ts-ignore
2
- import cassandra from "cassandra-driver"
3
- const { types } = cassandra
4
- import type { Model } from "../model"
5
- import { isValidFieldName } from "./queryParser"
6
-
7
- const stringTypes = new Set(["ascii", "text", "varchar", "inet"])
8
- const intTypes = new Set(["int", "smallint", "tinyint"])
9
- const floatTypes = new Set(["double", "float"])
10
- const longTypes = new Set(["bigint", "counter"])
11
-
12
- function isValidValue(value: any, expectedType: string): boolean {
13
- if (value === null || value === undefined) return true
14
-
15
- if (stringTypes.has(expectedType)) return typeof value === "string"
16
- if (intTypes.has(expectedType)) return Number.isInteger(value)
17
- if (floatTypes.has(expectedType)) return typeof value === "number"
18
- if (longTypes.has(expectedType)) {
19
- return (
20
- typeof value === "bigint" ||
21
- typeof value === "number" ||
22
- value instanceof types.Long
23
- )
24
- }
25
-
26
- switch (expectedType) {
27
- case "boolean":
28
- return typeof value === "boolean"
29
- case "decimal":
30
- return (
31
- typeof value === "number" ||
32
- typeof value === "string" ||
33
- value instanceof types.BigDecimal
34
- )
35
- case "varint":
36
- return (
37
- typeof value === "bigint" ||
38
- typeof value === "number" ||
39
- value instanceof types.Integer
40
- )
41
- case "timestamp":
42
- return (
43
- value instanceof Date ||
44
- typeof value === "number" ||
45
- typeof value === "string"
46
- )
47
- case "date":
48
- return typeof value === "string" || value instanceof types.LocalDate
49
- case "time":
50
- return typeof value === "string" || value instanceof types.LocalTime
51
- case "uuid":
52
- return typeof value === "string" || value instanceof types.Uuid
53
- case "timeuuid":
54
- return typeof value === "string" || value instanceof types.TimeUuid
55
- case "blob":
56
- return Buffer.isBuffer(value) || value instanceof Uint8Array
57
- }
58
-
59
- if (expectedType.startsWith("list<") || expectedType.startsWith("set<")) {
60
- return Array.isArray(value) || value instanceof Set
61
- }
62
- if (expectedType.startsWith("map<")) {
63
- return (
64
- typeof value === "object" && !Array.isArray(value) && value !== null
65
- )
66
- }
67
-
68
- return false
69
- }
70
-
71
- export default function typeChecker(model: Model<any>, data: any): boolean {
72
- if (!data || typeof data !== "object" || Array.isArray(data)) {
73
- throw new TypeError(
74
- `[${model.name}] Validation error: Data payload must be an object`,
75
- )
76
- }
77
-
78
- const fields = model.schema.fields
79
-
80
- for (const [key, value] of Object.entries(data)) {
81
- if (!isValidFieldName(fields, key)) {
82
- throw new Error(
83
- `[${model.name}] Validation error: Field '${key}' does not exist in schema`,
84
- )
85
- }
86
-
87
- const fieldConfig = fields[key]
88
- const expectedType = (fieldConfig.type || "text").toLowerCase()
89
-
90
- if (!isValidValue(value, expectedType)) {
91
- const receivedType = Array.isArray(value) ? "array" : typeof value
92
- throw new TypeError(
93
- `[${model.name}] Validation error: Invalid type for field '${key}'. ` +
94
- `Expected[${expectedType}], but received [${receivedType}]`,
95
- )
96
- }
97
- }
98
-
99
- return true
100
- }
package/tsconfig.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "$schema": "http://json.schemastore.org/tsconfig",
3
- "compilerOptions": {
4
- "allowSyntheticDefaultImports": true,
5
- "target": "ES2023",
6
- "module": "commonjs",
7
- "moduleResolution": "node",
8
- "declaration": true,
9
- "emitDecoratorMetadata": true,
10
- "experimentalDecorators": true,
11
- "strict": false,
12
- "noImplicitAny": false,
13
- "skipLibCheck": true,
14
- "preserveConstEnums": true,
15
- "sourceMap": true,
16
- "baseUrl": ".",
17
- },
18
- "include": ["**/*.ts"],
19
- }