pull/87/head
Zef Hemel 2022-10-05 10:35:01 +02:00
parent 66104a1ee7
commit 077d58d439
9 changed files with 106 additions and 258 deletions

29
mod.ts
View File

@ -9,22 +9,22 @@ export * as path from "https://deno.land/std@0.158.0/path/mod.ts";
export { readAll } from "https://deno.land/std@0.158.0/streams/conversion.ts"; export { readAll } from "https://deno.land/std@0.158.0/streams/conversion.ts";
export { export {
encode as b64encode,
decode as b64decode, decode as b64decode,
} from "https://deno.land/std/encoding/base64.ts"; encode as b64encode,
} from "https://deno.land/std@0.158.0/encoding/base64.ts";
export { export {
defaultHighlightStyle, defaultHighlightStyle,
Language,
LanguageSupport,
LanguageDescription,
syntaxHighlighting,
syntaxTree,
defineLanguageFacet, defineLanguageFacet,
languageDataProp,
foldNodeProp, foldNodeProp,
indentNodeProp, indentNodeProp,
Language,
languageDataProp,
LanguageDescription,
LanguageSupport,
ParseContext, ParseContext,
syntaxHighlighting,
syntaxTree,
} from "@codemirror/language"; } from "@codemirror/language";
export { markdown } from "https://esm.sh/@codemirror/lang-markdown@6.0.1?external=@codemirror/state"; export { markdown } from "https://esm.sh/@codemirror/lang-markdown@6.0.1?external=@codemirror/state";
export { export {
@ -45,19 +45,19 @@ export type {
LeafBlock, LeafBlock,
LeafBlockParser, LeafBlockParser,
MarkdownConfig, MarkdownConfig,
MarkdownExtension,
Table, Table,
TaskList, TaskList,
MarkdownExtension,
} from "https://esm.sh/@lezer/markdown"; } from "https://esm.sh/@lezer/markdown";
export { export {
Emoji,
GFM,
MarkdownParser, MarkdownParser,
parseCode, parseCode,
parser as baseParser, parser as baseParser,
GFM,
Subscript, Subscript,
Superscript, Superscript,
Emoji,
} from "https://esm.sh/@lezer/markdown"; } from "https://esm.sh/@lezer/markdown";
export type { SyntaxNode, Tree } from "https://esm.sh/@lezer/common"; export type { SyntaxNode, Tree } from "https://esm.sh/@lezer/common";
@ -77,9 +77,6 @@ export {
export type { KeyBinding } from "https://esm.sh/@codemirror/view@6.3.0?external=@codemirror/state"; export type { KeyBinding } from "https://esm.sh/@codemirror/view@6.3.0?external=@codemirror/state";
export { EditorSelection, EditorState, Text } from "@codemirror/state"; export { EditorSelection, EditorState, Text } from "@codemirror/state";
export type { StateCommand, ChangeSpec } from "@codemirror/state"; export type { ChangeSpec, StateCommand } from "@codemirror/state";
export { DB as SQLite3 } from "https://deno.land/x/sqlite/mod.ts"; export { Database as SQLite } from "https://deno.land/x/sqlite3@0.6.1/mod.ts";
// @deno-types="https://deno.land/x/dex@1.0.2/types/index.d.ts"
export { default as Dex } from "https://deno.land/x/dex@1.0.2/mod.ts";

View File

@ -1,5 +1,5 @@
import { sandboxCompile, sandboxCompileModule } from "../compile"; import { sandboxCompile, sandboxCompileModule } from "../compile";
import { SysCallMapping } from "../system"; import { SysCallMapping } from "../system.ts";
// TODO: FIgure out a better way to do this // TODO: FIgure out a better way to do this
const builtinModules = ["yaml", "handlebars"]; const builtinModules = ["yaml", "handlebars"];
@ -9,14 +9,14 @@ export function esbuildSyscalls(): SysCallMapping {
"tsc.analyze": async ( "tsc.analyze": async (
ctx, ctx,
filename: string, filename: string,
code: string code: string,
): Promise<any> => {}, ): Promise<any> => {},
"esbuild.compile": async ( "esbuild.compile": async (
ctx, ctx,
filename: string, filename: string,
code: string, code: string,
functionName?: string, functionName?: string,
excludeModules: string[] = [] excludeModules: string[] = [],
): Promise<string> => { ): Promise<string> => {
return await sandboxCompile( return await sandboxCompile(
filename, filename,
@ -24,12 +24,12 @@ export function esbuildSyscalls(): SysCallMapping {
functionName, functionName,
true, true,
[], [],
[...builtinModules, ...excludeModules] [...builtinModules, ...excludeModules],
); );
}, },
"esbuild.compileModule": async ( "esbuild.compileModule": async (
ctx, ctx,
moduleName: string moduleName: string,
): Promise<string> => { ): Promise<string> => {
return await sandboxCompileModule(moduleName, builtinModules); return await sandboxCompileModule(moduleName, builtinModules);
}, },

View File

@ -1,18 +1,11 @@
import { createSandbox } from "../environments/node_sandbox"; import { assertEquals } from "../../../test_dep.ts";
import { expect, test } from "@jest/globals"; import { SQLite } from "../../../mod.ts";
import { System } from "../system"; import { createSandbox } from "../environments/deno_sandbox.ts";
import { ensureTable, storeSyscalls } from "./store.knex_node"; import { System } from "../system.ts";
import knex from "knex"; import { ensureTable, storeSyscalls } from "./store.deno.ts";
import fs from "fs/promises";
test("Test store", async () => { Deno.test("Test store", async () => {
const db = knex({ const db = new SQLite(":memory:");
client: "better-sqlite3",
connection: {
filename: "test.db",
},
useNullAsDefault: true,
});
await ensureTable(db, "test_table"); await ensureTable(db, "test_table");
let system = new System("server"); let system = new System("server");
let syscalls = storeSyscalls(db, "test_table"); let syscalls = storeSyscalls(db, "test_table");
@ -33,9 +26,9 @@ test("Test store", async () => {
}, },
}, },
}, },
createSandbox createSandbox,
); );
expect(await plug.invoke("test1", [])).toBe("Pete"); assertEquals(await plug.invoke("test1", []), "Pete");
await system.unloadAll(); await system.unloadAll();
let dummyCtx: any = {}; let dummyCtx: any = {};
@ -74,8 +67,8 @@ test("Test store", async () => {
orderDesc: true, orderDesc: true,
}); });
expect(allRoberts.length).toBe(3); assertEquals(allRoberts.length, 3);
expect(allRoberts[0].key).toBe("petesr"); assertEquals(allRoberts[0].key, "petesr");
allRoberts = await syscalls["store.query"](dummyCtx, { allRoberts = await syscalls["store.query"](dummyCtx, {
filter: [{ op: "=", prop: "lastName", value: "Roberts" }], filter: [{ op: "=", prop: "lastName", value: "Roberts" }],
@ -83,8 +76,8 @@ test("Test store", async () => {
limit: 1, limit: 1,
}); });
expect(allRoberts.length).toBe(1); assertEquals(allRoberts.length, 1);
expect(allRoberts[0].key).toBe("petejr"); assertEquals(allRoberts[0].key, "petejr");
allRoberts = await syscalls["store.query"](dummyCtx, { allRoberts = await syscalls["store.query"](dummyCtx, {
filter: [ filter: [
@ -94,8 +87,8 @@ test("Test store", async () => {
orderBy: "age", orderBy: "age",
}); });
expect(allRoberts.length).toBe(1); assertEquals(allRoberts.length, 1);
expect(allRoberts[0].key).toBe("pete"); assertEquals(allRoberts[0].key, "pete");
// Delete the middle one // Delete the middle one
@ -107,9 +100,7 @@ test("Test store", async () => {
}); });
allRoberts = await syscalls["store.query"](dummyCtx, {}); allRoberts = await syscalls["store.query"](dummyCtx, {});
expect(allRoberts.length).toBe(2); assertEquals(allRoberts.length, 2);
await db.destroy(); db.close();
await fs.unlink("test.db");
}); });

View File

@ -1,8 +1,4 @@
import type { QueryBuilder } from "https://deno.land/x/dex@1.0.2/types/index.d.ts"; import { SQLite } from "../../../mod.ts";
import { RowObject } from "https://deno.land/x/sqlite/mod.ts";
import type { SQLite3 } from "../../../mod.ts";
import { Dex } from "../../../mod.ts";
import { SysCallMapping } from "../system.ts"; import { SysCallMapping } from "../system.ts";
export type Item = { export type Item = {
@ -16,24 +12,16 @@ export type KV = {
value: any; value: any;
}; };
const dex = Dex<Item>({ client: "sqlite3" }); export function ensureTable(db: SQLite, tableName: string) {
const stmt = db.prepare(
export function ensureTable(db: SQLite3, tableName: string) {
const result = db.query<[string]>(
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, `SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
[tableName],
); );
const result = stmt.all(tableName);
if (result.length === 0) { if (result.length === 0) {
const createQuery = dex.schema.createTable(tableName, (table) => { db.exec(`CREATE TABLE ${tableName} (key STRING PRIMARY KEY, value TEXT);`);
table.string("key");
table.text("value");
table.primary(["key"]);
}).toString();
db.query(createQuery);
console.log(`Created table ${tableName}`); console.log(`Created table ${tableName}`);
} }
return Promise.resolve();
} }
export type Query = { export type Query = {
@ -50,95 +38,110 @@ export type Filter = {
value: any; value: any;
}; };
export function queryToKnex( export function queryToSql(
queryBuilder: QueryBuilder<Item, any>,
query: Query, query: Query,
): QueryBuilder<Item, any> { ): { sql: string; params: any[] } {
const whereClauses: string[] = [];
const clauses: string[] = [];
const params: any[] = [];
if (query.filter) { if (query.filter) {
for (const filter of query.filter) { for (const filter of query.filter) {
queryBuilder = queryBuilder.andWhereRaw( whereClauses.push(
`json_extract(value, '$.${filter.prop}') ${filter.op} ?`, `json_extract(value, '$.${filter.prop}') ${filter.op} ?`,
[filter.value],
); );
params.push(filter.value);
} }
} }
if (query.limit) {
queryBuilder = queryBuilder.limit(query.limit);
}
if (query.orderBy) { if (query.orderBy) {
queryBuilder = queryBuilder.orderByRaw( clauses.push(
`json_extract(value, '$.${query.orderBy}') ${ `ORDER BY json_extract(value, '$.${query.orderBy}') ${
query.orderDesc ? "desc" : "asc" query.orderDesc ? "desc" : "asc"
}`, }`,
); );
} }
return queryBuilder; if (query.limit) {
clauses.push(`LIMIT ${query.limit}`);
}
return {
sql: whereClauses.length > 0
? `WHERE ${whereClauses.join(" AND ")} ${clauses.join(" ")}`
: clauses.join(" "),
params,
};
} }
function asyncQuery<T extends RowObject>( function asyncQuery<T extends Record<string, unknown>>(
db: SQLite3, db: SQLite,
query: QueryBuilder<any, any>, query: string,
...params: any[]
): Promise<T[]> { ): Promise<T[]> {
return Promise.resolve(db.queryEntries<T>(query.toString())); // console.log("Querying", query, params);
return Promise.resolve(db.prepare(query).all<T>(params));
} }
function asyncExecute( function asyncExecute(
db: SQLite3, db: SQLite,
query: QueryBuilder<any, any>, query: string,
): Promise<void> { ...params: any[]
return Promise.resolve(db.execute(query.toString())); ): Promise<number> {
// console.log("Exdecting", query, params);
return Promise.resolve(db.exec(query, params));
} }
export function storeSyscalls( export function storeSyscalls(
db: SQLite3, db: SQLite,
tableName: string, tableName: string,
): SysCallMapping { ): SysCallMapping {
const apiObj: SysCallMapping = { const apiObj: SysCallMapping = {
"store.delete": async (_ctx, key: string) => { "store.delete": async (_ctx, key: string) => {
await asyncExecute(db, dex(tableName).where({ key }).del()); await asyncExecute(db, `DELETE FROM ${tableName} WHERE key = ?`, key);
}, },
"store.deletePrefix": async (_ctx, prefix: string) => { "store.deletePrefix": async (_ctx, prefix: string) => {
await asyncExecute( await asyncExecute(
db, db,
dex(tableName).whereRaw(`"key" LIKE "${prefix}%"`).del(), `DELETE FROM ${tableName} WHERE key LIKE "?%"`,
prefix,
); );
}, },
"store.deleteQuery": async (_ctx, query: Query) => { "store.deleteQuery": async (_ctx, query: Query) => {
await asyncExecute(db, queryToKnex(dex(tableName), query).del()); const { sql, params } = queryToSql(query);
await asyncExecute(db, `DELETE FROM ${tableName} ${sql}`, ...params);
}, },
"store.deleteAll": async () => { "store.deleteAll": async () => {
await asyncExecute(db, dex(tableName).del()); await asyncExecute(db, `DELETE FROM ${tableName}`);
}, },
"store.set": async (_ctx, key: string, value: any) => { "store.set": async (_ctx, key: string, value: any) => {
await asyncExecute( await asyncExecute(
db, db,
dex(tableName).where({ key }).update("value", JSON.stringify(value)), `UPDATE ${tableName} SET value = ? WHERE key = ?`,
JSON.stringify(value),
key,
); );
if (db.changes === 0) { if (db.changes === 0) {
await asyncExecute( await asyncExecute(
db, db,
dex(tableName).insert({ `INSERT INTO ${tableName} (key, value) VALUES (?, ?)`,
key, key,
value: JSON.stringify(value), JSON.stringify(value),
}),
); );
} }
}, },
// TODO: Optimize // TODO: Optimize
"store.batchSet": async (ctx, kvs: KV[]) => { "store.batchSet": async (ctx, kvs: KV[]) => {
for (let { key, value } of kvs) { for (const { key, value } of kvs) {
await apiObj["store.set"](ctx, key, value); await apiObj["store.set"](ctx, key, value);
} }
}, },
"store.batchDelete": async (ctx, keys: string[]) => { "store.batchDelete": async (ctx, keys: string[]) => {
for (let key of keys) { for (const key of keys) {
await apiObj["store.delete"](ctx, key); await apiObj["store.delete"](ctx, key);
} }
}, },
"store.get": async (_ctx, key: string): Promise<any | null> => { "store.get": async (_ctx, key: string): Promise<any | null> => {
const result = await asyncQuery<Item>( const result = await asyncQuery<Item>(
db, db,
dex(tableName).where({ key }).select("value"), `SELECT value FROM ${tableName} WHERE key = ?`,
key,
); );
if (result.length) { if (result.length) {
return JSON.parse(result[0].value); return JSON.parse(result[0].value);
@ -146,13 +149,12 @@ export function storeSyscalls(
return null; return null;
} }
}, },
"store.queryPrefix": async (ctx, prefix: string) => { "store.queryPrefix": async (_ctx, prefix: string) => {
return ( return (
await asyncQuery<Item>( await asyncQuery<Item>(
db, db,
dex(tableName) `SELECT key, value FROM ${tableName} WHERE key LIKE "?%"`,
.andWhereRaw(`"key" LIKE "${prefix}%"`) prefix,
.select("key", "value"),
) )
).map(({ key, value }) => ({ ).map(({ key, value }) => ({
key, key,
@ -160,10 +162,12 @@ export function storeSyscalls(
})); }));
}, },
"store.query": async (_ctx, query: Query) => { "store.query": async (_ctx, query: Query) => {
const { sql, params } = queryToSql(query);
return ( return (
await asyncQuery<Item>( await asyncQuery<Item>(
db, db,
queryToKnex(dex(tableName), query).select("key", "value"), `SELECT key, value FROM ${tableName} ${sql}`,
...params,
) )
).map(({ key, value }: { key: string; value: string }) => ({ ).map(({ key, value }: { key: string; value: string }) => ({
key, key,

View File

@ -1,9 +0,0 @@
import { SQLite3 } from "../../../mod.ts";
import { storeSyscalls } from "./store.dex_deno.ts";
Deno.test("store.dex", async () => {
const db = new SQLite3(":memory:");
const syscalls = storeSyscalls(db, "test");
const fakeCtx = {} as any;
await syscalls["store.put"](fakeCtx, "key", { value: "value" });
});

View File

@ -1,133 +0,0 @@
import { Knex } from "knex";
import { SysCallMapping } from "../system";
export type Item = {
page: string;
key: string;
value: any;
};
export type KV = {
key: string;
value: any;
};
export async function ensureTable(db: Knex<any, unknown>, tableName: string) {
if (!(await db.schema.hasTable(tableName))) {
await db.schema.createTable(tableName, (table) => {
table.string("key");
table.text("value");
table.primary(["key"]);
});
console.log(`Created table ${tableName}`);
}
}
export type Query = {
filter?: Filter[];
orderBy?: string;
orderDesc?: boolean;
limit?: number;
select?: string[];
};
export type Filter = {
op: string;
prop: string;
value: any;
};
export function queryToKnex(
queryBuilder: Knex.QueryBuilder<Item, any>,
query: Query
): Knex.QueryBuilder<Item, any> {
if (query.filter) {
for (let filter of query.filter) {
queryBuilder = queryBuilder.andWhereRaw(
`json_extract(value, '$.${filter.prop}') ${filter.op} ?`,
[filter.value]
);
}
}
if (query.limit) {
queryBuilder = queryBuilder.limit(query.limit);
}
if (query.orderBy) {
queryBuilder = queryBuilder.orderByRaw(
`json_extract(value, '$.${query.orderBy}') ${
query.orderDesc ? "desc" : "asc"
}`
);
}
return queryBuilder;
}
export function storeSyscalls(
db: Knex<any, unknown>,
tableName: string
): SysCallMapping {
const apiObj: SysCallMapping = {
"store.delete": async (ctx, key: string) => {
await db<Item>(tableName).where({ key }).del();
},
"store.deletePrefix": async (ctx, prefix: string) => {
return db<Item>(tableName).andWhereLike("key", `${prefix}%`).del();
},
"store.deleteQuery": async (ctx, query: Query) => {
await queryToKnex(db<Item>(tableName), query).del();
},
"store.deleteAll": async (ctx) => {
await db<Item>(tableName).del();
},
"store.set": async (ctx, key: string, value: any) => {
let changed = await db<Item>(tableName)
.where({ key })
.update("value", JSON.stringify(value));
if (changed === 0) {
await db<Item>(tableName).insert({
key,
value: JSON.stringify(value),
});
}
},
// TODO: Optimize
"store.batchSet": async (ctx, kvs: KV[]) => {
for (let { key, value } of kvs) {
await apiObj["store.set"](ctx, key, value);
}
},
"store.batchDelete": async (ctx, keys: string[]) => {
for (let key of keys) {
await apiObj["store.delete"](ctx, key);
}
},
"store.get": async (ctx, key: string): Promise<any | null> => {
let result = await db<Item>(tableName).where({ key }).select("value");
if (result.length) {
return JSON.parse(result[0].value);
} else {
return null;
}
},
"store.queryPrefix": async (ctx, prefix: string) => {
return (
await db<Item>(tableName)
.andWhereLike("key", `${prefix}%`)
.select("key", "value")
).map(({ key, value }) => ({
key,
value: JSON.parse(value),
}));
},
"store.query": async (ctx, query: Query) => {
return (
await queryToKnex(db<Item>(tableName), query).select("key", "value")
).map(({ key, value }: { key: string; value: string }) => ({
key,
value: JSON.parse(value),
}));
},
};
return apiObj;
}

View File

@ -1,14 +0,0 @@
{
"include": ["bin/*", "environments/*", "hooks/*", "syscalls/*", "*"],
"compilerOptions": {
"target": "esnext",
"strict": true,
"moduleResolution": "node",
"module": "esnext",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"jsx": "react-jsx",
"downlevelIteration": true
}
}

BIN
test.db Normal file

Binary file not shown.

12
test_dep.ts Normal file
View File

@ -0,0 +1,12 @@
export {
assert,
assertEquals,
AssertionError,
assertMatch,
assertNotEquals,
assertThrows,
equal,
fail,
unimplemented,
unreachable,
} from "https://deno.land/std@0.158.0/testing/asserts.ts";