imean-cassandra-orm
Version:
cassandra orm
484 lines (477 loc) • 16.4 kB
JavaScript
;
var cassandraDriver = require('cassandra-driver');
var zod = require('zod');
// src/client.ts
function createZodSchemaForFields(schema, filter) {
const zodSchema = {};
Object.entries(schema).forEach(([key, config]) => {
if (filter(config)) {
let baseSchema;
switch (config.type) {
case "text":
baseSchema = zod.z.string();
break;
case "int":
case "bigint":
case "varint":
baseSchema = zod.z.number().int();
break;
case "float":
case "double":
baseSchema = zod.z.number();
break;
case "boolean":
baseSchema = zod.z.boolean();
break;
case "timestamp":
baseSchema = zod.z.date();
break;
case "uuid":
baseSchema = zod.z.union([zod.z.string(), zod.z.instanceof(cassandraDriver.types.Uuid)]).transform(
(val) => val instanceof cassandraDriver.types.Uuid ? val.toString() : val
);
break;
default:
baseSchema = zod.z.any();
}
zodSchema[key] = config.flags.optional ? baseSchema.optional() : baseSchema;
}
});
return zod.z.object(zodSchema);
}
function createZodSchema(schema) {
return createZodSchemaForFields(schema, () => true);
}
function createPartitionKeyZodSchema(schema) {
return createZodSchemaForFields(
schema,
(config) => config.flags.partitionKey
);
}
function createClusteringKeyZodSchema(schema) {
return createZodSchemaForFields(
schema,
(config) => !!config.flags.clusteringKey
);
}
// src/model.ts
var Model = class {
constructor(client, schema, tableName, keyspace) {
this.client = client;
this.schema = schema;
this.tableName = tableName;
this.keyspace = keyspace;
this.partitionKeys = Object.entries(schema).filter(([_, config]) => config.flags.partitionKey).map(([key]) => key);
this.clusteringKeys = Object.entries(schema).filter(([_, config]) => config.flags.clusteringKey).map(([key]) => key);
this.zodSchemas = {
entity: createZodSchema(schema),
partitionKey: createPartitionKeyZodSchema(schema),
clusteringKey: createClusteringKeyZodSchema(schema)
};
}
tableName;
partitionKeys;
clusteringKeys;
keyspace;
zodSchemas;
// 获取表结构 CQL
getTableSchema() {
const columns = Object.entries(this.schema).map(([name, config]) => {
const type = config.type;
const staticFlag = config.flags.static ? " STATIC" : "";
return `${name} ${type}${staticFlag}`;
}).join(",\n ");
const primaryKey = this.clusteringKeys.length > 0 ? `((${this.partitionKeys.join(", ")}), ${this.clusteringKeys.join(
", "
)})` : `((${this.partitionKeys.join(", ")}))`;
const clusteringOrder = this.clusteringKeys.map((key) => {
const config = this.schema[key];
if (typeof config.flags.clusteringKey === "object") {
return `${key} ${config.flags.clusteringKey.order}`;
}
return `${key} ASC`;
}).join(", ");
return `CREATE TABLE IF NOT EXISTS ${this.keyspace}.${this.tableName} (
${columns},
PRIMARY KEY ${primaryKey}
)${clusteringOrder ? ` WITH CLUSTERING ORDER BY (${clusteringOrder})` : ""};`;
}
// 同步表结构
async syncSchema(force = false) {
await this.client.metadata.refreshKeyspace(this.keyspace);
const tableInfo = await this.client.metadata.getTable(
this.keyspace,
this.tableName
);
if (!tableInfo) {
await this.client.execute(this.getTableSchema(), [], { prepare: true });
return;
}
const currentColumns = new Map(
tableInfo.columns.map((col) => [col.name.toLowerCase(), col])
);
const newFields = Object.entries(this.schema).filter(
([fieldName]) => !currentColumns.has(fieldName.toLowerCase())
);
const changes = [];
Object.entries(this.schema).forEach(([fieldName, config]) => {
const currentColumn = currentColumns.get(fieldName.toLowerCase());
if (!currentColumn) return;
const currentType = this.getCqlTypeFromCode(currentColumn.type.code);
const newType = config.type;
if (currentType !== newType) {
changes.push(
`\u5B57\u6BB5 ${fieldName} \u7684\u7C7B\u578B\u4ECE ${currentType} \u53D8\u66F4\u4E3A ${newType}`
);
}
const isCurrentPartitionKey = tableInfo.partitionKeys.some(
(key) => key.name.toLowerCase() === fieldName.toLowerCase()
);
if (isCurrentPartitionKey !== config.flags.partitionKey) {
changes.push(
`\u5B57\u6BB5 ${fieldName} \u7684\u5206\u533A\u952E\u72B6\u6001\u4ECE ${isCurrentPartitionKey} \u53D8\u66F4\u4E3A ${config.flags.partitionKey}`
);
}
const isCurrentClusteringKey = tableInfo.clusteringKeys.some(
(key) => key.name.toLowerCase() === fieldName.toLowerCase()
);
const isClusteringKey = !!config.flags.clusteringKey;
if (isCurrentClusteringKey !== isClusteringKey) {
changes.push(
`\u5B57\u6BB5 ${fieldName} \u7684\u805A\u7C7B\u952E\u72B6\u6001\u4ECE ${isCurrentClusteringKey} \u53D8\u66F4\u4E3A ${isClusteringKey}`
);
}
});
if (newFields.length > 0 && changes.length === 0) {
for (const [fieldName, config] of newFields) {
await this.client.execute(
`ALTER TABLE ${this.keyspace}.${this.tableName} ADD ${fieldName} ${config.type}`,
[],
{ prepare: true }
);
}
return;
}
if (changes.length > 0) {
if (!force) {
throw new Error(
`\u68C0\u6D4B\u5230\u8868\u7ED3\u6784\u53D8\u66F4\u9700\u8981\u5220\u9664\u91CD\u5EFA\uFF1A
${changes.join(
"\n"
)}
\u8BF7\u786E\u8BA4\u540E\u4F7F\u7528 force=true \u53C2\u6570\u6267\u884C`
);
}
await this.client.execute(
`DROP TABLE ${this.keyspace}.${this.tableName}`,
[],
{ prepare: true }
);
await this.client.execute(this.getTableSchema(), [], { prepare: true });
}
}
// 根据类型代码获取 CQL 类型
getCqlTypeFromCode(code) {
const typeMap = {
[cassandraDriver.types.dataTypes.text]: "text",
[cassandraDriver.types.dataTypes.int]: "int",
[cassandraDriver.types.dataTypes.bigint]: "bigint",
[cassandraDriver.types.dataTypes.varint]: "varint",
[cassandraDriver.types.dataTypes.boolean]: "boolean",
[cassandraDriver.types.dataTypes.timestamp]: "timestamp",
[cassandraDriver.types.dataTypes.uuid]: "uuid",
[cassandraDriver.types.dataTypes.float]: "float",
[cassandraDriver.types.dataTypes.double]: "double",
[cassandraDriver.types.dataTypes.blob]: "blob",
[cassandraDriver.types.dataTypes.decimal]: "decimal",
[cassandraDriver.types.dataTypes.inet]: "inet",
[cassandraDriver.types.dataTypes.timeuuid]: "timeuuid",
[cassandraDriver.types.dataTypes.duration]: "duration",
[cassandraDriver.types.dataTypes.map]: "map",
[cassandraDriver.types.dataTypes.set]: "set",
[cassandraDriver.types.dataTypes.list]: "list"
};
const typeName = typeMap[code];
if (!typeName) {
throw new Error(`Unknown type code: ${code}`);
}
return typeName;
}
// 将查询结果转换为我们的类型
convertResultToType(result) {
const converted = {};
Object.keys(this.schema).forEach((key) => {
const lowerKey = key.toLowerCase();
const resultKey = Object.keys(result).find(
(k) => k.toLowerCase() === lowerKey
);
if (resultKey) {
converted[key] = result[resultKey];
}
});
return this.zodSchemas.entity.parse(converted);
}
// 构建查询条件
buildWhereClause(partitionFields, clusteringFields) {
const conditions = [];
this.partitionKeys.forEach((key) => {
const value = partitionFields[key];
if (value !== void 0) {
conditions.push(`${key} = ?`);
}
});
if (clusteringFields) {
this.clusteringKeys.forEach((key) => {
const value = clusteringFields[key];
if (value !== void 0) {
conditions.push(`${key} = ?`);
}
});
}
return conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
}
// 构建查询参数
buildQueryParams(partitionFields, clusteringFields) {
const params = [];
this.partitionKeys.forEach((key) => {
const value = partitionFields[key];
if (value !== void 0) {
params.push(value);
}
});
if (clusteringFields) {
this.clusteringKeys.forEach((key) => {
const value = clusteringFields[key];
if (value !== void 0) {
params.push(value);
}
});
}
return params;
}
// 验证数据
validate(data) {
try {
this.zodSchemas.entity.parse(data);
return true;
} catch (error) {
return false;
}
}
// 获取验证错误
getValidationErrors(data) {
try {
this.zodSchemas.entity.parse(data);
return null;
} catch (error) {
return error;
}
}
// 查询方法
async findAll(partitionFields, clusteringFields) {
const whereClause = this.buildWhereClause(
partitionFields,
clusteringFields
);
const params = this.buildQueryParams(partitionFields, clusteringFields);
const query = `SELECT * FROM ${this.keyspace}.${this.tableName} ${whereClause}`;
const result = await this.client.execute(query, params, { prepare: true });
return result.rows.map((row) => this.convertResultToType(row));
}
async findOne(partitionFields, clusteringFields) {
const whereClause = this.buildWhereClause(
partitionFields,
clusteringFields
);
const params = this.buildQueryParams(partitionFields, clusteringFields);
const query = `SELECT * FROM ${this.keyspace}.${this.tableName} ${whereClause} LIMIT 1`;
const result = await this.client.execute(query, params, { prepare: true });
return result.rows[0] ? this.convertResultToType(result.rows[0]) : null;
}
// 添加记录方法
async create(data, options) {
const { ttl = 0, ...restOptions } = options || {};
const columns = Object.keys(data).join(", ");
const placeholders = Object.keys(data).map(() => "?").join(", ");
const params = Object.values(data);
const usingTTL = ttl > 0 ? `USING TTL ${ttl}` : "";
const query = `INSERT INTO ${this.keyspace}.${this.tableName} (${columns}) VALUES (${placeholders}) ${usingTTL}`.trim();
await this.client.execute(query, params, { prepare: true, ...restOptions });
}
// 更新记录方法
async update(partitionFields, data) {
const setClause = Object.keys(data).map((key) => `${key} = ?`).join(", ");
const whereClause = this.buildWhereClause(partitionFields);
const params = [
...Object.values(data),
...this.buildQueryParams(partitionFields)
];
const query = `UPDATE ${this.keyspace}.${this.tableName} SET ${setClause} ${whereClause}`;
await this.client.execute(query, params, { prepare: true });
}
// 删除记录方法
async delete(partitionFields, clusteringFields) {
const whereClause = this.buildWhereClause(
partitionFields,
clusteringFields
);
const params = this.buildQueryParams(partitionFields, clusteringFields);
const query = `DELETE FROM ${this.keyspace}.${this.tableName} ${whereClause}`;
await this.client.execute(query, params, { prepare: true });
}
// 优化后的批量查询方法(支持静态列)
async findAllOptimized(partitionFields, clusteringFields) {
const staticColumns = Object.entries(this.schema).filter(([_, config]) => config.flags.static).map(([name]) => name);
const nonStaticColumns = Object.entries(this.schema).filter(([_, config]) => !config.flags.static).map(([name]) => name);
const whereClause = this.buildWhereClause(
partitionFields,
clusteringFields
);
const params = this.buildQueryParams(partitionFields, clusteringFields);
const [staticResult, nonStaticResult] = await Promise.all([
// 查询静态列
staticColumns.length > 0 ? this.client.execute(
`SELECT ${staticColumns.join(", ")} FROM ${this.keyspace}.${this.tableName} ${whereClause} LIMIT 1`,
params,
{ prepare: true }
) : Promise.resolve({ rows: [] }),
// 查询非静态列
this.client.execute(
`SELECT ${nonStaticColumns.join(", ")} FROM ${this.keyspace}.${this.tableName} ${whereClause}`,
params,
{ prepare: true }
)
]);
const staticData = staticResult.rows[0] || {};
return nonStaticResult.rows.map((row) => ({
...staticData,
...row
}));
}
};
var uuid = () => cassandraDriver.types.Uuid.random().toString();
// src/client.ts
var Client2 = class {
constructor(cassandraClient) {
this.cassandraClient = cassandraClient;
}
models = /* @__PURE__ */ new Map();
// 使用 keyspace
async useKeyspace(keyspace, options) {
const checkKeyspaceQuery = `
SELECT keyspace_name
FROM system_schema.keyspaces
WHERE keyspace_name = ?`;
const result = await this.cassandraClient.execute(
checkKeyspaceQuery,
[keyspace],
{ prepare: true }
);
if (result.rows.length === 0) {
let createKeyspaceQuery = `CREATE KEYSPACE IF NOT EXISTS ${keyspace} WITH REPLICATION = { 'class' : '${options.class}'`;
if (options.class === "SimpleStrategy") {
createKeyspaceQuery += `, 'replication_factor' : ${options.replication_factor || 1}`;
} else if (options.class === "NetworkTopologyStrategy" && options.datacenters) {
const datacenterConfigs = Object.entries(options.datacenters).map(([dc, rf]) => `'${dc}' : ${rf}`).join(", ");
createKeyspaceQuery += `, ${datacenterConfigs}`;
}
createKeyspaceQuery += " }";
await this.cassandraClient.execute(createKeyspaceQuery, [], {
prepare: true
});
}
}
// 创建模型
createModel(schema, tableName, keyspace) {
const model = new Model(this.cassandraClient, schema, tableName, keyspace);
this.models.set(tableName, model);
return model;
}
// 同步所有表结构
async syncSchema(force = false) {
for (const model of this.models.values()) {
await model.syncSchema(force);
}
}
// 获取原始 Cassandra 客户端
getRawClient() {
return this.cassandraClient;
}
// 关闭连接
async close() {
await this.cassandraClient.shutdown();
}
};
// src/field.ts
var FieldBuilder = class _FieldBuilder {
constructor(type, flags = {
partitionKey: false,
clusteringKey: false,
optional: false,
static: false
}) {
this.type = type;
this.flags = flags;
}
partitionKey() {
const newFlags = {
...this.flags,
partitionKey: true
};
return new _FieldBuilder(this.type, newFlags);
}
clusteringKey(order) {
const newFlags = {
...this.flags,
clusteringKey: order ? { order } : true
};
return new _FieldBuilder(this.type, newFlags);
}
optional() {
const newFlags = {
...this.flags,
optional: true
};
return new _FieldBuilder(this.type, newFlags);
}
static() {
const newFlags = {
...this.flags,
static: true
};
return new _FieldBuilder(this.type, newFlags);
}
};
var createField = (type) => {
return new FieldBuilder(type);
};
var Field = {
// 文本类型
text: () => createField("text"),
uuid: () => createField("uuid"),
// 数值类型
int: () => createField("int"),
bigint: () => createField("bigint"),
varint: () => createField("varint"),
decimal: () => createField("decimal"),
float: () => createField("float"),
double: () => createField("double"),
// 布尔类型
boolean: () => createField("boolean"),
// 时间类型
timestamp: () => createField("timestamp"),
// 二进制类型
blob: () => createField("blob"),
// 集合类型
list: () => createField("list"),
set: () => createField("set"),
map: () => createField("map")
};
exports.Client = Client2;
exports.Field = Field;
exports.FieldBuilder = FieldBuilder;
exports.Model = Model;
exports.createClusteringKeyZodSchema = createClusteringKeyZodSchema;
exports.createPartitionKeyZodSchema = createPartitionKeyZodSchema;
exports.createZodSchema = createZodSchema;
exports.uuid = uuid;