391 lines
12 KiB
TypeScript
391 lines
12 KiB
TypeScript
import { Property, createPrismaSchemaBuilder } from "@mrleebo/prisma-ast";
|
|
import { readAsync } from "fs-jetpack";
|
|
//@ts-ignore
|
|
import { Prisma } from "../../app/db/db";
|
|
import { dir } from "./dir";
|
|
import { gunzipAsync } from "./gzip";
|
|
|
|
export type DBArg = {
|
|
db: string;
|
|
table: string;
|
|
action: string;
|
|
params: any[];
|
|
};
|
|
|
|
export const execQuery = async (args: DBArg, prisma: any) => {
|
|
const { table, action, params } = args;
|
|
|
|
if (action === "batch_upsert") {
|
|
const { arg } = params as unknown as {
|
|
arg?: {
|
|
table: string;
|
|
where: any;
|
|
data: any[];
|
|
mode: "field" | "relation";
|
|
};
|
|
};
|
|
if (arg) {
|
|
const { table, where, data } = arg;
|
|
const mode = arg.mode || "field";
|
|
if (table && where && data) {
|
|
const transactions = [];
|
|
|
|
const schema_path = dir("app/db/prisma/schema.prisma");
|
|
const schema = createPrismaSchemaBuilder(await readAsync(schema_path));
|
|
const schema_table = schema.findByType("model", { name: table });
|
|
|
|
if (schema_table) {
|
|
let pks: Property[] = [];
|
|
for (const col of schema_table.properties) {
|
|
if (col.type === "field" && !col.array) {
|
|
if (col.attributes && col.attributes?.length > 0) {
|
|
const is_pk = col.attributes.find((e) => e.name === "id");
|
|
if (is_pk) {
|
|
pks.push(col);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (pks.length > 0) {
|
|
if (Object.keys(where.length > 0)) {
|
|
const select = {} as any;
|
|
for (const pk of pks) {
|
|
select[pk.name] = true;
|
|
}
|
|
const existing: any[] = await prisma[table].findMany({
|
|
where,
|
|
select,
|
|
});
|
|
|
|
const updates = [] as any[];
|
|
const inserts = [] as any[];
|
|
const deletes = [] as any[];
|
|
const exists_idx = new Set<number>();
|
|
|
|
for (const row of data) {
|
|
const found = existing.find((item, idx) => {
|
|
for (const pk of pks) {
|
|
if (item[pk.name] !== row[pk.name]) return false;
|
|
}
|
|
exists_idx.add(idx);
|
|
return true;
|
|
});
|
|
|
|
if (mode === "field") {
|
|
if (found) {
|
|
updates.push({ ...row, ...where });
|
|
} else {
|
|
inserts.push({ ...row, ...where });
|
|
}
|
|
} else {
|
|
if (found) {
|
|
updates.push({ ...row });
|
|
} else {
|
|
inserts.push({ ...row });
|
|
}
|
|
}
|
|
}
|
|
|
|
if (exists_idx.size !== existing.length) {
|
|
for (const [k, v] of Object.entries(existing)) {
|
|
if (!exists_idx.has(parseInt(k))) {
|
|
deletes.push(v);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (inserts.length > 0) {
|
|
transactions.push(
|
|
prisma[table].createMany({
|
|
data: inserts,
|
|
skipDuplicates: true,
|
|
})
|
|
);
|
|
}
|
|
|
|
if (updates.length > 0) {
|
|
for (const item of updates) {
|
|
const where = {} as any;
|
|
for (const pk of pks) {
|
|
where[pk.name] = item[pk.name];
|
|
delete item[pk.name];
|
|
}
|
|
|
|
transactions.push(
|
|
prisma[table].update({ data: item, where, select })
|
|
);
|
|
}
|
|
}
|
|
|
|
if (deletes.length > 0) {
|
|
for (const item of deletes) {
|
|
const where = {} as any;
|
|
for (const pk of pks) {
|
|
where[pk.name] = item[pk.name];
|
|
}
|
|
transactions.push(prisma[table].delete({ where }));
|
|
}
|
|
}
|
|
|
|
return await prisma.$transaction(transactions);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (action === "batch_update") {
|
|
const { table, batch } = params as unknown as {
|
|
table?: { table: string; data: any; where: any }[];
|
|
batch?: { table: string; data: any; where: any }[];
|
|
};
|
|
|
|
const promises = [] as any[];
|
|
|
|
const b = table || batch;
|
|
|
|
if (b) {
|
|
try {
|
|
for (const item of b) {
|
|
if (
|
|
item.table &&
|
|
!!item.data &&
|
|
!!item.where &&
|
|
Object.entries(item.where).length > 0 &&
|
|
Object.entries(item.data).length > 0
|
|
) {
|
|
const tableInstance = prisma[item.table];
|
|
if (tableInstance) {
|
|
promises.push(
|
|
tableInstance.updateMany({ where: item.where, data: item.data })
|
|
);
|
|
}
|
|
}
|
|
}
|
|
await Promise.all(promises);
|
|
} catch (e: any) {
|
|
throw new Error(e.message);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
if (action.startsWith("schema_")) {
|
|
const schema_path = dir("app/db/prisma/schema.prisma");
|
|
const schema = createPrismaSchemaBuilder(await readAsync(schema_path));
|
|
if (action === "schema_tables") {
|
|
const tables = schema.findAllByType("model", {}).map((e) => e?.name);
|
|
return tables || [];
|
|
} else {
|
|
const schema_table = schema.findByType("model", { name: table });
|
|
const columns = {} as Record<
|
|
string,
|
|
{
|
|
is_pk: boolean;
|
|
type: string;
|
|
optional: boolean;
|
|
db_type: string;
|
|
default?: any;
|
|
}
|
|
>;
|
|
const rels = {} as Record<
|
|
string,
|
|
| {
|
|
type: "has-many";
|
|
to: { table: string; fields: string[] };
|
|
from: { table: string; fields: string[] };
|
|
}
|
|
| {
|
|
type: "has-one";
|
|
to: { table: string; fields: string[] };
|
|
from: { table: string; fields: string[] };
|
|
}
|
|
>;
|
|
if (schema_table) {
|
|
if (action === "schema_rels") {
|
|
for (const col of schema_table.properties) {
|
|
if (
|
|
col.type === "field" &&
|
|
(!!col.array || (col.attributes && col.attributes?.length > 0))
|
|
) {
|
|
if (col.array) {
|
|
if (typeof col.fieldType === "string") {
|
|
const target = schema.findByType("model", {
|
|
name: col.fieldType,
|
|
});
|
|
|
|
if (target) {
|
|
const field = target.properties.find((e) => {
|
|
if (e.type === "field" && e.fieldType === table) {
|
|
return true;
|
|
}
|
|
});
|
|
if (field && field.type === "field") {
|
|
const rel = field.attributes?.find(
|
|
(e) => e.kind === "field"
|
|
);
|
|
|
|
if (rel && rel.args) {
|
|
const { field, ref } = getFieldAndRef(
|
|
rel,
|
|
target,
|
|
table
|
|
);
|
|
|
|
if (target && ref) {
|
|
rels[col.name] = {
|
|
type: "has-many",
|
|
to: field,
|
|
from: ref,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (col.attributes) {
|
|
const rel = col.attributes.find(
|
|
(e) => e.type === "attribute" && e.name === "relation"
|
|
);
|
|
if (rel && typeof col.fieldType === "string") {
|
|
const target = schema.findByType("model", {
|
|
name: col.fieldType,
|
|
});
|
|
const { field, ref } = getFieldAndRef(rel, target, table);
|
|
|
|
rels[col.name] = {
|
|
type: "has-one",
|
|
to: {
|
|
table: field.table,
|
|
fields: ref.fields,
|
|
},
|
|
from: {
|
|
table: ref.table,
|
|
fields: field.fields,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return rels;
|
|
} else if (action === "schema_columns") {
|
|
for (const col of schema_table.properties) {
|
|
if (col.type === "field" && !col.array) {
|
|
if (col.attributes && col.attributes?.length > 0) {
|
|
const attr = col.attributes.find(
|
|
(e) => e.name !== "id" && e.name !== "default"
|
|
);
|
|
|
|
const default_val = col.attributes.find(
|
|
(e) => e.name === "default"
|
|
);
|
|
const is_pk = col.attributes.find((e) => e.name === "id");
|
|
|
|
let type = "String";
|
|
if (typeof col.fieldType === "string") type = col.fieldType;
|
|
|
|
if ((attr && attr.name !== "relation") || !attr) {
|
|
columns[col.name] = {
|
|
is_pk: !!is_pk,
|
|
type: type.toLowerCase(),
|
|
optional: !!col.optional,
|
|
db_type: attr
|
|
? attr.name.toLowerCase()
|
|
: type.toLowerCase(),
|
|
default: default_val,
|
|
};
|
|
}
|
|
} else if (typeof col.fieldType === "string") {
|
|
columns[col.name] = {
|
|
is_pk: false,
|
|
type: col.fieldType.toLowerCase(),
|
|
optional: !!col.optional,
|
|
db_type: col.fieldType.toLowerCase(),
|
|
default: null,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
return columns;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const tableInstance = prisma[table];
|
|
|
|
if (tableInstance) {
|
|
if (action === "query" && table.startsWith("$query")) {
|
|
const gzip = params as unknown as string;
|
|
|
|
const u8 = new Uint8Array([...atob(gzip)].map((c) => c.charCodeAt(0)));
|
|
const json = JSON.parse((await gunzipAsync(u8)).toString("utf8"));
|
|
|
|
if (table === "$queryRawUnsafe") {
|
|
return await tableInstance.bind(prisma)(...json);
|
|
}
|
|
|
|
if (Array.isArray(json)) {
|
|
const q = json.shift();
|
|
return await tableInstance.bind(prisma)(Prisma.sql(q, ...json));
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
const method = tableInstance[action];
|
|
|
|
if (method) {
|
|
if (action === "deleteMany") {
|
|
if (!params[0] || (params[0] && Object.keys(params[0]).length === 0))
|
|
throw new Error("deleteMany without condition is forbidden");
|
|
|
|
if (params[0] && params[0].where) {
|
|
const filtered = Object.values(params[0].where).filter(
|
|
(e) => e
|
|
).length;
|
|
if (filtered === 0)
|
|
throw new Error("deleteMany without condition is forbidden");
|
|
}
|
|
}
|
|
|
|
const result = await method(...params);
|
|
|
|
if (!result) {
|
|
return JSON.stringify(result);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
};
|
|
|
|
const getFieldAndRef = (rel: any, target: any, table: string) => {
|
|
let field = null as unknown as { table: string; fields: string[] };
|
|
let ref = null as unknown as { table: string; fields: string[] };
|
|
for (const e of rel.args) {
|
|
if (
|
|
typeof e.value === "object" &&
|
|
!Array.isArray(e.value) &&
|
|
e.value.type === "keyValue" &&
|
|
typeof e.value.value === "object" &&
|
|
!Array.isArray(e.value.value) &&
|
|
e.value.value.type === "array"
|
|
) {
|
|
if (e.value.key === "fields") {
|
|
field = {
|
|
table: target.name,
|
|
fields: e.value.value.args,
|
|
};
|
|
} else if (e.value.key === "references") {
|
|
ref = {
|
|
table: table,
|
|
fields: e.value.value.args,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
return { field, ref };
|
|
};
|