├── .gitignore ├── README.md ├── assets └── redux-tweet.png └── ts ├── .changeset ├── README.md └── config.json ├── build-all ├── CHANGELOG.md ├── package.json └── tsconfig.json ├── connectors ├── sqlite3-connector │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.cjs │ ├── package.json │ ├── src │ │ ├── Connection.ts │ │ ├── Mutex.ts │ │ └── index.ts │ └── tsconfig.json └── wa-sqlite-connector │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── Connection.ts │ ├── ConnectionPool.ts │ ├── index.ts │ ├── sqliteInit.ts │ └── trace.ts │ └── tsconfig.json ├── extensions └── authorization-grammar │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ └── index.ts │ └── tsconfig.json ├── ides └── vscode │ ├── .npmignore │ ├── .vscode │ └── launch.json │ ├── .vscodeignore │ ├── CHANGELOG.md │ ├── README.md │ ├── language-configuration.json │ ├── package.json │ ├── syntaxes │ └── aphrodite.tmLanguage.json │ └── vsc-extension-quickstart.md ├── integration-tests └── data-model │ ├── .gitignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ ├── domain.vlcn │ └── domain │ │ ├── User.ts │ │ └── generated │ │ ├── User.sqlite.sql │ │ ├── UserBase.ts │ │ ├── UserQuery.ts │ │ ├── UserSpec.ts │ │ ├── exports-node-sql.ts │ │ ├── exports-sql.ts │ │ ├── exports.ts │ │ └── types.d.ts │ └── tsconfig.json ├── package.json ├── packages ├── cache │ ├── .gitignore │ ├── .npmignore │ ├── .pnpm-debug.log │ ├── CHANGELOG.md │ ├── babel.config.cjs │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ └── Cache.test.ts │ │ ├── cache.ts │ │ ├── index.ts │ │ └── queryCache.ts │ └── tsconfig.json ├── codegen-api │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── CodegenFile.ts │ │ ├── CodegenPipeline.ts │ │ ├── CodegenStep.ts │ │ ├── __tests__ │ │ │ ├── CodegenFile.test.ts │ │ │ └── uniqueImports.test.ts │ │ ├── index.ts │ │ └── uniqueImports.ts │ └── tsconfig.json ├── codegen-cli │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ └── cli.test.ts │ │ └── cli.ts │ └── tsconfig.json ├── codegen-sql │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.cjs │ ├── package.json │ ├── src │ │ ├── GenSqlTableSchema.ts │ │ ├── SqlFile.ts │ │ ├── __tests__ │ │ │ └── GenSqlTableSchema.test.ts │ │ └── index.ts │ └── tsconfig.json ├── codegen-ts │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.cjs │ ├── package.json │ ├── src │ │ ├── GenSQLExports.ts │ │ ├── GenSQLExports_node.ts │ │ ├── GenSchemaExports.ts │ │ ├── GenTypes_d_ts.ts │ │ ├── GenTypescriptModel.ts │ │ ├── GenTypescriptModelManualMethodsClass.ts │ │ ├── GenTypescriptQuery.ts │ │ ├── GenTypescriptSpec.ts │ │ ├── TypescriptFile.ts │ │ ├── __tests__ │ │ │ ├── GenTypescriptModel.test.ts │ │ │ ├── GenTypescriptQuery.test.ts │ │ │ ├── GenTypesriptSpec.test.ts │ │ │ └── tsUtils.test.ts │ │ ├── index.ts │ │ └── tsUtils.ts │ └── tsconfig.json ├── config │ ├── .npmignore │ ├── CHANGELOG.md │ ├── babel.config.cjs │ ├── package.json │ ├── src │ │ ├── context.ts │ │ ├── index.ts │ │ └── vulcan-config.ts │ └── tsconfig.json ├── feature-gates │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── grammar-extension-api │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── id │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.cjs │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ └── id.test.ts │ │ ├── id.ts │ │ ├── index.ts │ │ └── uuidv7.ts │ └── tsconfig.json ├── instrument │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── tracer.ts │ └── tsconfig.json ├── lazy-idb │ └── notes.md ├── memory-db │ └── notes.md ├── migration │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ └── automigrate.test.ts │ │ ├── autoMigrate.ts │ │ ├── bootstrap.ts │ │ └── index.ts │ └── tsconfig.json ├── model-persisted │ ├── CHANGELOG.md │ ├── babel.config.cjs │ ├── package.json │ ├── src │ │ ├── AsyncPersistedModel.ts │ │ ├── PersistTracker.ts │ │ ├── PersistedModel.ts │ │ ├── SyncPersistedModel.ts │ │ ├── datasetKey.ts │ │ ├── index.ts │ │ ├── modelGenMemo.ts │ │ ├── persistor.ts │ │ ├── spec.ts │ │ └── syncAsyncCommon.ts │ └── tsconfig.json ├── model-pfr │ ├── MemoryDB.ts │ ├── README.md │ └── Table.ts ├── model │ ├── CHANGELOG.md │ ├── babel.config.cjs │ ├── package.json │ ├── src │ │ ├── Model.ts │ │ ├── __tests__ │ │ │ └── Model.test.ts │ │ └── index.ts │ └── tsconfig.json ├── query │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.cjs │ ├── package.json │ ├── src │ │ ├── ChunkIterable.ts │ │ ├── CountLoadExpression.ts │ │ ├── Expression.ts │ │ ├── ExpressionVisitor.ts │ │ ├── Field.ts │ │ ├── HopPlan.ts │ │ ├── ModelLoadExpression.ts │ │ ├── Plan.ts │ │ ├── Predicate.ts │ │ ├── Query.ts │ │ ├── QueryFactory.ts │ │ ├── StorageAdapter.ts │ │ ├── __tests__ │ │ │ └── ChunkIterable.test.ts │ │ ├── explain │ │ │ ├── __tests__ │ │ │ │ └── orderPlans.test.ts │ │ │ ├── orderPlans.ts │ │ │ └── printPlan.ts │ │ ├── index.ts │ │ ├── live │ │ │ ├── LiveResult.ts │ │ │ └── __tests__ │ │ │ │ └── LiveResult.test.ts │ │ ├── memory │ │ │ ├── MemoryHopChunkIterable.ts │ │ │ ├── MemoryHopExpression.ts │ │ │ ├── MemoryHopQuery.ts │ │ │ ├── MemorySourceChunkIterable.ts │ │ │ ├── MemorySourceExpression.ts │ │ │ └── MemorySourceQuery.ts │ │ ├── sql │ │ │ ├── SQLExpression.ts │ │ │ ├── SQLHopChunkIterable.ts │ │ │ ├── SQLHopExpression.ts │ │ │ ├── SQLHopQuery.ts │ │ │ ├── SQLSourceChunkIterable.ts │ │ │ ├── SQLSourceExpression.ts │ │ │ ├── SQLSourceQuery.ts │ │ │ ├── __tests__ │ │ │ │ ├── SQLSourceChunkIterable.test.ts │ │ │ │ └── specAndOpsToQuery.test.ts │ │ │ └── specAndOpsToQuery.ts │ │ └── trace.ts │ └── tsconfig.json ├── react │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── babel.config.cjs │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ └── cache.test.ts │ │ ├── createHooks.ts │ │ ├── hooks.ts │ │ ├── index.ts │ │ └── useSync.ts │ └── tsconfig.json ├── runtime │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── schema-api │ ├── .gitignore │ ├── .npmignore │ ├── .pnpm-debug.log │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── schema │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.cjs │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ └── schemaFilePipeline.test.ts │ │ ├── compile.ts │ │ ├── edge.ts │ │ ├── field.ts │ │ ├── index.ts │ │ ├── module.ts │ │ ├── node.ts │ │ ├── parser │ │ │ ├── __tests__ │ │ │ │ ├── condense.test.ts │ │ │ │ ├── documentSchemaFile.ts │ │ │ │ ├── documentType.test.ts │ │ │ │ ├── grammar.test.ts │ │ │ │ ├── parse.test.ts │ │ │ │ └── testSchemaFile.ts │ │ │ ├── condense.ts │ │ │ ├── condenseEntities.ts │ │ │ ├── ohm │ │ │ │ ├── grammar.ohm │ │ │ │ └── grammar.ts │ │ │ └── parse.ts │ │ ├── runtimeConfig.ts │ │ ├── type.ts │ │ └── validate.ts │ └── tsconfig.json ├── sql │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── babel.config.cjs │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ └── sqlite.test.ts │ │ ├── index.ts │ │ └── sql.ts │ └── tsconfig.json ├── sync │ └── notes.md ├── util │ ├── CHANGELOG.md │ ├── babel.config.cjs │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ └── index.test.ts │ │ ├── index.ts │ │ ├── observe.ts │ │ └── static.ts │ └── tsconfig.json ├── value │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.cjs │ ├── notes.md │ ├── package.json │ ├── src │ │ ├── History.ts │ │ ├── ObservableValue.ts │ │ ├── PersistedValue.ts │ │ ├── Value.ts │ │ ├── __tests__ │ │ │ ├── History.test.ts │ │ │ ├── ObservableValue.test.ts │ │ │ ├── Value.test.ts │ │ │ ├── demo.test.ts │ │ │ ├── memory.test.ts │ │ │ ├── shared-value-tests.ts │ │ │ └── transaction.test.ts │ │ ├── index.ts │ │ ├── memory.ts │ │ └── transaction.ts │ └── tsconfig.json └── zone │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.cjs │ ├── package.json │ ├── src │ ├── __tests__ │ │ └── index.test.ts │ ├── dexie │ │ ├── README.md │ │ ├── functions │ │ │ ├── chaining-functions.js │ │ │ └── utils.ts │ │ ├── globals │ │ │ └── global.ts │ │ └── helpers │ │ │ ├── debug.ts │ │ │ └── promise.js │ └── index.ts │ └── tsconfig.json ├── tsconfig-lib.json └── turbo.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | tsconfig.tsbuildinfo 4 | .turbo/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A collection of projects to simplify [aphrodite.sh](https://aphrodite.sh) and move the state of programming forward. 2 | 3 | Hephaestus is too hard to say and spell [hence the name Vulcan](https://en.wikipedia.org/wiki/Hephaestus#:~:text=Hephaestus%27s%20Roman%20counterpart%20is%20Vulcan). 4 | 5 | `aphrodite.sh` apis are cumbersome in a few areas. The main culprits: 6 | - a reliance on the concept of "changesets" (fixed here via [transactional memory](https://github.com/aphrodite-sh/vulcan/tree/main/ts/packages/value/README.md)) 7 | - requiring a "context" parameter to be passed to all function calls (potentially fixed via [context-provider](https://github.com/aphrodite-sh/vulcan/tree/main/ts/packages/context-provider) or more likely just a config object) 8 | - encouraging "named" mutations (removed from next build of aphrodite). 9 | 10 | Other culprits are: 11 | 12 | - the query APIs being 100% async even for in-memory data stores 13 | - asking the developer to remember to await their writes if they want to do an immediate read-after-write 14 | - ddl? maybe? This is probably a necessary complexity and unlocks future simplicity 15 | 16 | The current most interesting part of vulcan [is transactional memory](https://github.com/aphrodite-sh/vulcan/tree/main/ts/packages/value/README.md). 17 | 18 | 19 | This repo will be merged into `vlcn-io/vlcn-orm` at a future date. 20 | -------------------------------------------------------------------------------- /assets/redux-tweet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlcn-io/model/7d3850fc5d1f0b2ed52d721edb898fccc21cb90f/assets/redux-tweet.png -------------------------------------------------------------------------------- /ts/.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /ts/.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } -------------------------------------------------------------------------------- /ts/build-all/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @vlcn.io/build-all 2 | 3 | ## 0.3.0 4 | 5 | ### Minor Changes 6 | 7 | - publish for testing 8 | -------------------------------------------------------------------------------- /ts/build-all/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/build-all", 3 | "version": "0.3.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "devDependencies": { 7 | "typescript": "^4.8.2" 8 | }, 9 | "scripts": { 10 | "clean": "tsc --build --clean", 11 | "build": "tsc --build", 12 | "watch": "tsc --build -w", 13 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 14 | }, 15 | "jest": { 16 | "testMatch": [ 17 | "**/__tests__/**/*.test.js" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ts/build-all/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [ 9 | { 10 | "path": "../packages/cache" 11 | }, 12 | { 13 | "path": "../packages/codegen-api" 14 | }, 15 | { 16 | "path": "../packages/codegen-cli" 17 | }, 18 | { 19 | "path": "../packages/codegen-sql" 20 | }, 21 | { 22 | "path": "../packages/codegen-ts" 23 | }, 24 | { 25 | "path": "../packages/config" 26 | }, 27 | { 28 | "path": "../packages/feature-gates" 29 | }, 30 | { 31 | "path": "../packages/grammar-extension-api" 32 | }, 33 | { 34 | "path": "../packages/id" 35 | }, 36 | { 37 | "path": "../packages/instrument" 38 | }, 39 | { 40 | "path": "../packages/migration" 41 | }, 42 | { 43 | "path": "../packages/model" 44 | }, 45 | { 46 | "path": "../packages/model-persisted" 47 | }, 48 | { 49 | "path": "../packages/query" 50 | }, 51 | { 52 | "path": "../packages/schema" 53 | }, 54 | { 55 | "path": "../packages/schema-api" 56 | }, 57 | { 58 | "path": "../packages/sql" 59 | }, 60 | { 61 | "path": "../packages/util" 62 | }, 63 | { 64 | "path": "../packages/value" 65 | }, 66 | { 67 | "path": "../packages/zone" 68 | }, 69 | { 70 | "path": "../connectors/sqlite3-connector" 71 | }, 72 | { 73 | "path": "../connectors/wa-sqlite-connector" 74 | } 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /ts/connectors/sqlite3-connector/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /ts/connectors/sqlite3-connector/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/connectors/sqlite3-connector/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @aphro/sqlite3-connector 2 | 3 | ## 0.4.0 4 | 5 | ### Minor Changes 6 | 7 | - publish for testing 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - @vlcn.io/runtime@0.4.0 13 | 14 | ## 0.3.2 15 | 16 | ### Patch Changes 17 | 18 | - Export queries and specs, move connectors to own packages, fix #43 and other bugs 19 | - Updated dependencies 20 | - @aphro/runtime-ts@0.3.8 21 | 22 | ## 0.3.1 23 | 24 | ### Patch Changes 25 | 26 | - transaction support 27 | - Updated dependencies 28 | - @aphro/runtime-ts@0.3.7 29 | -------------------------------------------------------------------------------- /ts/connectors/sqlite3-connector/README.md: -------------------------------------------------------------------------------- 1 | # sqlite3-connector 2 | 3 | Enable connectivity between Aphrodite and `sqlite3` -------------------------------------------------------------------------------- /ts/connectors/sqlite3-connector/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /ts/connectors/sqlite3-connector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/sqlite3-connector", 3 | "version": "0.4.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/connectors/sqlite3-connector" 10 | }, 11 | "dependencies": { 12 | "@vlcn.io/runtime": "workspace:*", 13 | "@types/sqlite3": "^3.1.8", 14 | "sqlite3": "^5.0.11" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "^7.18.13", 18 | "@babel/preset-env": "^7.18.10", 19 | "@types/jest": "^28.1.8", 20 | "@types/node": "^18.7.13", 21 | "jest": "^29.0.1", 22 | "typescript": "^4.8.2" 23 | }, 24 | "scripts": { 25 | "clean": "tsc --build --clean", 26 | "build": "tsc --build", 27 | "watch": "tsc --build -w", 28 | "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js", 29 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 30 | }, 31 | "jest": { 32 | "testMatch": [ 33 | "**/__tests__/**/*.test.js" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ts/connectors/sqlite3-connector/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DBResolver } from "@vlcn.io/runtime"; 2 | import { basicSqliteResolver } from "@vlcn.io/runtime"; 3 | import { createConnection } from "./Connection.js"; 4 | export { createConnection } from "./Connection.js"; 5 | 6 | /** 7 | * Convenience function to create a connection to absurd-sql and return 8 | * a db resolver that resolves to that connection. 9 | * 10 | * You should _only_ ever call `openDbAndCreateResolver` one time from your application. 11 | * After calling `createResolver`, attach the provided resolver to `Context` and/or pass 12 | * your resolver instance around to where it is needed. 13 | * 14 | * Only call this once since each call will try to start up a new sqlite instance. 15 | * 16 | * @returns DBResolver 17 | */ 18 | export async function openDbAndCreateResolver( 19 | dbName: string, 20 | file: string | null 21 | ): Promise { 22 | const conn = await createConnection(file); 23 | return basicSqliteResolver(dbName, conn); 24 | } 25 | -------------------------------------------------------------------------------- /ts/connectors/sqlite3-connector/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [{ "path": "../../packages/runtime" }] 9 | } 10 | -------------------------------------------------------------------------------- /ts/connectors/wa-sqlite-connector/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /ts/connectors/wa-sqlite-connector/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @aphro/wa-sqlite-connector 2 | 3 | ## 0.4.0 4 | 5 | ### Minor Changes 6 | 7 | - publish for testing 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - @vlcn.io/instrument@0.1.0 13 | - @vlcn.io/runtime@0.4.0 14 | 15 | ## 0.3.3 16 | 17 | ### Patch Changes 18 | 19 | - Export queries and specs, move connectors to own packages, fix #43 and other bugs 20 | - Updated dependencies 21 | - @aphro/instrument@0.0.6 22 | - @aphro/runtime-ts@0.3.8 23 | 24 | ## 0.3.2 25 | 26 | ### Patch Changes 27 | 28 | - fix imports to appease webpack 29 | 30 | ## 0.3.1 31 | 32 | ### Patch Changes 33 | 34 | - transaction support 35 | - Updated dependencies 36 | - @aphro/instrument@0.0.5 37 | - @aphro/runtime-ts@0.3.7 38 | 39 | ## 0.3.0 40 | 41 | ### Minor Changes 42 | 43 | - depend on `runtime-ts` to fix pkg mismatches, convenience mutations, named mutations replace unnmaed mutations 44 | - depend on `runtime-ts` to prevent pkg version mismatch, convenience `mutatations` accessor 45 | 46 | ### Patch Changes 47 | 48 | - @aphro/runtime-ts@0.3.6 49 | 50 | ## 0.2.4 51 | 52 | ### Patch Changes 53 | 54 | - Strict mode for typescript, useEffect vs useSyncExternalStore, useLiveResult hook 55 | - Updated dependencies 56 | - @aphro/context-runtime-ts@0.3.4 57 | - @aphro/instrument@0.0.4 58 | - @aphro/sql-ts@0.2.4 59 | 60 | ## 0.2.3 61 | 62 | ### Patch Changes 63 | 64 | - rebuild -- last publish had a clobbered version of pnpm 65 | - Updated dependencies 66 | - @aphro/context-runtime-ts@0.3.3 67 | - @aphro/instrument@0.0.3 68 | - @aphro/sql-ts@0.2.3 69 | 70 | ## 0.2.2 71 | 72 | ### Patch Changes 73 | 74 | - workaround to adhere to strict mode in generated code #43 75 | - Updated dependencies 76 | - @aphro/context-runtime-ts@0.3.2 77 | - @aphro/instrument@0.0.2 78 | - @aphro/sql-ts@0.2.2 79 | 80 | ## 0.2.1 81 | 82 | ### Patch Changes 83 | 84 | - generate bootstrapping utilities 85 | - Updated dependencies 86 | - @aphro/context-runtime-ts@0.3.1 87 | - @aphro/sql-ts@0.2.1 88 | -------------------------------------------------------------------------------- /ts/connectors/wa-sqlite-connector/README.md: -------------------------------------------------------------------------------- 1 | # wa-sqlite 2 | 3 | Adapter to let `Aphrodite` query `wa-sqlite` 4 | -------------------------------------------------------------------------------- /ts/connectors/wa-sqlite-connector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/wa-sqlite-connector", 3 | "version": "0.4.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/connectors/wa-sqlite-connector" 10 | }, 11 | "dependencies": { 12 | "@vlcn.io/instrument": "workspace:*", 13 | "@vlcn.io/runtime": "workspace:*", 14 | "@opentelemetry/api": "^1.1.0", 15 | "@strut/counter": "^0.0.11", 16 | "wa-sqlite": "github:rhashimoto/wa-sqlite#buildless" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.18.13", 20 | "@babel/preset-env": "^7.18.10", 21 | "@types/jest": "^28.1.8", 22 | "jest": "^29.0.1", 23 | "typescript": "^4.8.2" 24 | }, 25 | "scripts": { 26 | "clean": "tsc --build --clean", 27 | "build": "tsc --build", 28 | "watch": "tsc --build -w", 29 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 30 | }, 31 | "jest": { 32 | "testMatch": [ 33 | "**/__tests__/**/*.test.js" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ts/connectors/wa-sqlite-connector/src/ConnectionPool.ts: -------------------------------------------------------------------------------- 1 | import { sql, SQLQuery, SQLResolvedDB } from "@vlcn.io/runtime"; 2 | import createConnection, { Connection } from "./Connection.js"; 3 | 4 | // we should remove the connection pool for wa-sqlite 5 | // could be useful in other environments, however. 6 | // https://github.com/rhashimoto/wa-sqlite/discussions/52 7 | class ConnectionPool { 8 | type = "sql"; 9 | #writeConnection: Connection; 10 | #txConnection: Connection; 11 | #readConnections: Connection[]; 12 | 13 | constructor(connections: Connection[]) { 14 | this.#writeConnection = connections[0]; 15 | this.#txConnection = connections[1]; 16 | this.#readConnections = connections.slice(2); 17 | } 18 | 19 | read(q: SQLQuery): Promise { 20 | const conn = 21 | this.#readConnections[ 22 | Math.floor(Math.random() * this.#readConnections.length) 23 | ]; 24 | return conn.read(q); 25 | } 26 | 27 | async begin(): Promise { 28 | } 29 | 30 | async commit(): Promise { 31 | } 32 | 33 | async rollback(): Promise { 34 | } 35 | 36 | write(q: SQLQuery): Promise { 37 | return this.#writeConnection.write(q); 38 | } 39 | 40 | async transact(cb: (conn: SQLResolvedDB) => Promise): Promise { 41 | return this.#txConnection.transact(cb); 42 | } 43 | 44 | dispose(): void { 45 | this.#writeConnection.dispose(); 46 | this.#txConnection.dispose(); 47 | this.#readConnections.forEach((rc) => rc.dispose()); 48 | } 49 | } 50 | 51 | // We create a pool so we can have a dedicated thread for transactions 52 | // rather than implementing and managing mutexes in JS 53 | export default async function createPool( 54 | dbName: string 55 | ): Promise { 56 | const connectons = await Promise.all( 57 | Array.from({ length: 3 }).map((_) => createConnection(dbName)) 58 | ); 59 | 60 | return new ConnectionPool(connectons); 61 | } 62 | -------------------------------------------------------------------------------- /ts/connectors/wa-sqlite-connector/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DBResolver, basicSqliteResolver } from "@vlcn.io/runtime"; 2 | import createPool from "./ConnectionPool.js"; 3 | 4 | /** 5 | * Convenience function to create a connection to absurd-sql and return 6 | * a db resolver that resolves to that connection. 7 | * 8 | * You should _only_ ever call `createResolver` one time from your application. 9 | * After calling `createResolver`, attach the provided resolver to `Context` and/or pass 10 | * your resolver instance around to where it is needed. 11 | * 12 | * Only call this once since each call will try to start up a new sqlite instance. 13 | * 14 | * @returns DBResolver 15 | */ 16 | export async function openDbAndCreateResolver( 17 | dbName: string 18 | ): Promise { 19 | const pool = await createPool(dbName); 20 | return basicSqliteResolver(dbName, pool); 21 | } 22 | -------------------------------------------------------------------------------- /ts/connectors/wa-sqlite-connector/src/sqliteInit.ts: -------------------------------------------------------------------------------- 1 | import SQLiteAsyncESMFactory from 'wa-sqlite/dist/wa-sqlite-async.mjs'; 2 | import * as SQLite from 'wa-sqlite'; 3 | // @ts-ignore 4 | import { IDBBatchAtomicVFS } from 'wa-sqlite/src/examples/IDBBatchAtomicVFS.js'; 5 | 6 | let api: SQLiteAPI | null = null; 7 | export default async function getSqliteApi(): Promise { 8 | if (api != null) { 9 | return api; 10 | } 11 | 12 | const module = await SQLiteAsyncESMFactory({ 13 | locateFile(file: string) { 14 | return file; 15 | }, 16 | }); 17 | const sqlite3 = SQLite.Factory(module); 18 | sqlite3.vfs_register(new IDBBatchAtomicVFS('idb-batch-atomic', { durability: 'relaxed' })); 19 | 20 | return sqlite3; 21 | } 22 | -------------------------------------------------------------------------------- /ts/connectors/wa-sqlite-connector/src/trace.ts: -------------------------------------------------------------------------------- 1 | import { tracer, Tracer } from "@vlcn.io/instrument"; 2 | 3 | const t: Tracer = tracer("@vlcn.io/wa-sqlite-connector", "0.2.3"); 4 | 5 | export default t; 6 | -------------------------------------------------------------------------------- /ts/connectors/wa-sqlite-connector/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["./src/"], 9 | "references": [ 10 | { "path": "../../packages/runtime" }, 11 | { "path": "../../packages/instrument" } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /ts/extensions/authorization-grammar/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /ts/extensions/authorization-grammar/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/extensions/authorization-grammar/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @aphro/authorization-grammar 2 | 3 | ## 0.2.6 4 | 5 | ### Patch Changes 6 | 7 | - Export queries and specs, move connectors to own packages, fix #43 and other bugs 8 | 9 | ## 0.2.5 10 | 11 | ### Patch Changes 12 | 13 | - transaction support 14 | 15 | ## 0.2.4 16 | 17 | ### Patch Changes 18 | 19 | - Strict mode for typescript, useEffect vs useSyncExternalStore, useLiveResult hook 20 | 21 | ## 0.2.3 22 | 23 | ### Patch Changes 24 | 25 | - rebuild -- last publish had a clobbered version of pnpm 26 | 27 | ## 0.2.2 28 | 29 | ### Patch Changes 30 | 31 | - workaround to adhere to strict mode in generated code #43 32 | 33 | ## 0.2.1 34 | 35 | ### Patch Changes 36 | 37 | - generate bootstrapping utilities 38 | 39 | ## 0.2.0 40 | 41 | ### Minor Changes 42 | 43 | - Simplify manual files, change output dir for generated code, allow caching in live queries, simplify 1 to 1 edge fetches 44 | 45 | ## 0.1.3 46 | 47 | ### Patch Changes 48 | 49 | - update dependency on strut/utils, enable manual methods for models 50 | 51 | ## 0.1.2 52 | 53 | ### Patch Changes 54 | 55 | - allow ephemeral nodes. allow type expressions for fields. 56 | 57 | ## 0.1.1 58 | 59 | ### Patch Changes 60 | 61 | - in-memory model support 62 | 63 | ## 0.1.0 64 | 65 | ### Minor Changes 66 | 67 | - Support for standalone / junction edges 68 | 69 | ## 0.0.10 70 | 71 | ### Patch Changes 72 | 73 | - count/orderBy/take implementation, support for NOT NULL, empty queries 74 | 75 | ## 0.0.9 76 | 77 | ### Patch Changes 78 | 79 | - Fix casing errors on filesystem 80 | 81 | ## 0.0.8 82 | 83 | ### Patch Changes 84 | 85 | - graphql support, 'create table if not exists' for easier bootstrapping, @databases connection support 86 | 87 | ## 0.0.7 88 | 89 | ### Patch Changes 90 | 91 | - full todomvc example, no partiall generated mutators, removal of knexjs 92 | 93 | ## 0.0.6 94 | 95 | ### Patch Changes 96 | 97 | - enable running in the browser, implement reactive queries 98 | -------------------------------------------------------------------------------- /ts/extensions/authorization-grammar/README.md: -------------------------------------------------------------------------------- 1 | # Auth Grammar 2 | 3 | Extends `Aphrodite SDL` with a grammar for defining row, column and edge level visibility. 4 | 5 | Before: 6 | ``` 7 | User as Node { 8 | id: ID 9 | name: NaturalLanguage 10 | password: PBKDF2 11 | } 12 | ``` 13 | 14 | After: 15 | ``` 16 | User as Node { 17 | id: ID 18 | name: NaturalLanguage 19 | password: PBKDF2 & Auth { red: [AllowIf((viewer, node) => node.id === viewer.id)] } # field level privacy 20 | } & Authorization { # object level privacy 21 | read: [ 22 | AlwaysAllow # everyone can see everyone 23 | ] 24 | write: [ 25 | AllowIf((viewer, node) => node.id === viewer.id) # only user themselves can update themselves 26 | ] 27 | } 28 | ``` 29 | 30 | > TODO: this should also extend the `mutation` grammar to allow auth on specific mutations. -------------------------------------------------------------------------------- /ts/extensions/authorization-grammar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aphro/authorization-grammar", 3 | "version": "0.2.6", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/tantaman/aphrodite.git", 9 | "directory": "extensions/authorization-grammar" 10 | }, 11 | "devDependencies": { 12 | "typescript": "^4.8.2" 13 | }, 14 | "scripts": { 15 | "clean": "tsc --build --clean", 16 | "build": "tsc --build", 17 | "watch": "tsc --build -w", 18 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 19 | }, 20 | "jest": { 21 | "testMatch": [ 22 | "**/__tests__/**/*.test.js" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ts/extensions/authorization-grammar/src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlcn-io/model/7d3850fc5d1f0b2ed52d721edb898fccc21cb90f/ts/extensions/authorization-grammar/src/index.ts -------------------------------------------------------------------------------- /ts/extensions/authorization-grammar/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib/", // path to output directory 4 | "sourceMap": true, // allow sourcemap support 5 | "strictNullChecks": true, // enable strict null checks as a best practice 6 | "module": "esnext", // specify module code generation 7 | "target": "esnext", // specify ECMAScript target version 8 | "skipLibCheck": true, 9 | "moduleResolution": "Node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "baseUrl": "./src/", 13 | "declaration": true, 14 | "allowJs": true, 15 | "composite": true, 16 | "declarationMap": true, 17 | "incremental": true, 18 | "rootDir": "./src" 19 | }, 20 | "include": ["./src/"], 21 | "references": [] 22 | } 23 | -------------------------------------------------------------------------------- /ts/ides/vscode/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/ides/vscode/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /ts/ides/vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | .gitignore 4 | vsc-extension-quickstart.md 5 | -------------------------------------------------------------------------------- /ts/ides/vscode/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.2.6 4 | 5 | ### Patch Changes 6 | 7 | - Export queries and specs, move connectors to own packages, fix #43 and other bugs 8 | 9 | ## 0.2.5 10 | 11 | ### Patch Changes 12 | 13 | - transaction support 14 | 15 | ## 0.2.4 16 | 17 | ### Patch Changes 18 | 19 | - Strict mode for typescript, useEffect vs useSyncExternalStore, useLiveResult hook 20 | 21 | ## 0.2.3 22 | 23 | ### Patch Changes 24 | 25 | - rebuild -- last publish had a clobbered version of pnpm 26 | 27 | ## 0.2.2 28 | 29 | ### Patch Changes 30 | 31 | - workaround to adhere to strict mode in generated code #43 32 | 33 | ## 0.2.1 34 | 35 | ### Patch Changes 36 | 37 | - generate bootstrapping utilities 38 | 39 | ## 0.2.0 40 | 41 | ### Minor Changes 42 | 43 | - Simplify manual files, change output dir for generated code, allow caching in live queries, simplify 1 to 1 edge fetches 44 | 45 | ## 0.1.3 46 | 47 | ### Patch Changes 48 | 49 | - update dependency on strut/utils, enable manual methods for models 50 | 51 | ## 0.1.2 52 | 53 | ### Patch Changes 54 | 55 | - allow ephemeral nodes. allow type expressions for fields. 56 | 57 | ## 0.1.1 58 | 59 | ### Patch Changes 60 | 61 | - in-memory model support 62 | 63 | ## 0.1.0 64 | 65 | ### Minor Changes 66 | 67 | - Support for standalone / junction edges 68 | 69 | ## 0.0.10 70 | 71 | ### Patch Changes 72 | 73 | - count/orderBy/take implementation, support for NOT NULL, empty queries 74 | 75 | ## 0.0.9 76 | 77 | ### Patch Changes 78 | 79 | - Fix casing errors on filesystem 80 | 81 | ## 0.0.8 82 | 83 | ### Patch Changes 84 | 85 | - graphql support, 'create table if not exists' for easier bootstrapping, @databases connection support 86 | 87 | ## 0.0.7 88 | 89 | ### Patch Changes 90 | 91 | - full todomvc example, no partiall generated mutators, removal of knexjs 92 | 93 | ## 0.0.6 94 | 95 | ### Patch Changes 96 | 97 | - enable running in the browser, implement reactive queries 98 | 99 | All notable changes to the "aphrodite" extension will be documented in this file. 100 | 101 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 102 | 103 | ## [Unreleased] 104 | 105 | - Initial release 106 | -------------------------------------------------------------------------------- /ts/ides/vscode/README.md: -------------------------------------------------------------------------------- 1 | # aphrodite README 2 | 3 | This is the README for your extension "aphrodite". After writing up a brief description, we recommend including the following sections. 4 | 5 | ## Features 6 | 7 | Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file. 8 | 9 | For example if there is an image subfolder under your extension project workspace: 10 | 11 | \!\[feature X\]\(images/feature-x.png\) 12 | 13 | > Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow. 14 | 15 | ## Requirements 16 | 17 | If you have any requirements or dependencies, add a section describing those and how to install and configure them. 18 | 19 | ## Extension Settings 20 | 21 | Include if your extension adds any VS Code settings through the `contributes.configuration` extension point. 22 | 23 | For example: 24 | 25 | This extension contributes the following settings: 26 | 27 | * `myExtension.enable`: enable/disable this extension 28 | * `myExtension.thing`: set to `blah` to do something 29 | 30 | ## Known Issues 31 | 32 | Calling out known issues can help limit users opening duplicate issues against your extension. 33 | 34 | ## Release Notes 35 | 36 | Users appreciate release notes as you update your extension. 37 | 38 | ### 1.0.0 39 | 40 | Initial release of ... 41 | 42 | ### 1.0.1 43 | 44 | Fixed issue #. 45 | 46 | ### 1.1.0 47 | 48 | Added features X, Y, and Z. 49 | 50 | ----------------------------------------------------------------------------------------------------------- 51 | 52 | ## Working with Markdown 53 | 54 | **Note:** You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts: 55 | 56 | * Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux) 57 | * Toggle preview (`Shift+CMD+V` on macOS or `Shift+Ctrl+V` on Windows and Linux) 58 | * Press `Ctrl+Space` (Windows, Linux) or `Cmd+Space` (macOS) to see a list of Markdown snippets 59 | 60 | ### For more information 61 | 62 | * [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown) 63 | * [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/) 64 | 65 | **Enjoy!** 66 | -------------------------------------------------------------------------------- /ts/ides/vscode/language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "#" 4 | }, 5 | "brackets": [ 6 | ["<", ">"], 7 | ["(", ")"], 8 | ["{", "}"] 9 | ], 10 | "autoClosingPairs": [ 11 | ["<", ">"], 12 | ["(", ")"], 13 | ["{", "}"] 14 | ], 15 | "surroundingPairs": [ 16 | ["<", ">"], 17 | ["(", ")"] 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /ts/ides/vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aphrodite", 3 | "displayName": "aphrodite", 4 | "description": "Support for Aphrodite schemas", 5 | "version": "0.2.6", 6 | "engines": { 7 | "vscode": "^1.65.0" 8 | }, 9 | "categories": [ 10 | "Programming Languages" 11 | ], 12 | "contributes": { 13 | "languages": [ 14 | { 15 | "id": "aphrodite", 16 | "aliases": [ 17 | "Aphrodite", 18 | "aphrodite" 19 | ], 20 | "extensions": [ 21 | ".aphro" 22 | ], 23 | "configuration": "./language-configuration.json" 24 | } 25 | ], 26 | "grammars": [ 27 | { 28 | "language": "aphrodite", 29 | "scopeName": "source.aphro", 30 | "path": "./syntaxes/aphrodite.tmLanguage.json" 31 | } 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ts/ides/vscode/syntaxes/aphrodite.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "Aphrodite", 4 | "patterns": [ 5 | { 6 | "include": "#keywords" 7 | }, 8 | { 9 | "include": "#entities" 10 | }, 11 | { 12 | "include": "#variables" 13 | } 14 | ], 15 | "repository": { 16 | "keywords": { 17 | "patterns": [ 18 | { 19 | "name": "keyword.control.aphrodite", 20 | "match": "\\b(Node|NodeTrait|Traits|Edge|ReadPrivacy|Index|InboundEdges|OutboundEdges|Invert)\\b" 21 | }, 22 | { 23 | "name": "keyword.operator.aphrodite", 24 | "match": "\\b(as|\\|)\\b" 25 | }, 26 | { 27 | "name": "constant.language.aphrodite", 28 | "match": "\\b(ID|Map|Array|Timestamp|Currency|bool|int32|int64|float32|float64|uint32|uint64|string|Enumeration|NaturalLanguage|Bitmask)\\b" 29 | } 30 | ] 31 | }, 32 | "entities": { 33 | "patterns": [ 34 | { 35 | "name": "entity.name.type.aphrodite", 36 | "match": "\\b([A-Za-z][0-9A-Za-z_]*) as\\b" 37 | } 38 | ] 39 | }, 40 | "variables": { 41 | "patterns": [ 42 | { 43 | "name": "variable.parameter.aphrodite", 44 | "match": "(<.*>)" 45 | } 46 | ] 47 | } 48 | }, 49 | "scopeName": "source.aphro" 50 | } 51 | -------------------------------------------------------------------------------- /ts/ides/vscode/vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your language support and define the location of the grammar file that has been copied into your extension. 7 | * `syntaxes/aphrodite.tmLanguage.json` - this is the Text mate grammar file that is used for tokenization. 8 | * `language-configuration.json` - this is the language configuration, defining the tokens that are used for comments and brackets. 9 | 10 | ## Get up and running straight away 11 | 12 | * Make sure the language configuration settings in `language-configuration.json` are accurate. 13 | * Press `F5` to open a new window with your extension loaded. 14 | * Create a new file with a file name suffix matching your language. 15 | * Verify that syntax highlighting works and that the language configuration settings are working. 16 | 17 | ## Make changes 18 | 19 | * You can relaunch the extension from the debug toolbar after making changes to the files listed above. 20 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 21 | 22 | ## Add more language features 23 | 24 | * To add features such as intellisense, hovers and validators check out the VS Code extenders documentation at https://code.visualstudio.com/docs 25 | 26 | ## Install your extension 27 | 28 | * To start using your extension with Visual Studio Code copy it into the `/.vscode/extensions` folder and restart Code. 29 | * To share your extension with the world, read on https://code.visualstudio.com/docs about publishing an extension. 30 | -------------------------------------------------------------------------------- /ts/integration-tests/data-model/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /ts/integration-tests/data-model/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @aphro/integration-tests-shared 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - publish for testing 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - @vlcn.io/runtime@0.4.0 13 | 14 | ## 0.0.2 15 | 16 | ### Patch Changes 17 | 18 | - Export queries and specs, move connectors to own packages, fix #43 and other bugs 19 | - Updated dependencies 20 | - @aphro/runtime-ts@0.3.8 21 | -------------------------------------------------------------------------------- /ts/integration-tests/data-model/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/integration-tests-data-model", 3 | "private": true, 4 | "version": "0.1.0", 5 | "main": "lib/index.js", 6 | "type": "module", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/vulcan-sh/vulcan.git", 10 | "directory": "integration-tests/data-model" 11 | }, 12 | "dependencies": { 13 | "@vlcn.io/runtime": "workspace:*" 14 | }, 15 | "devDependencies": { 16 | "@vlcn.io/codegen-cli": "workspace:*", 17 | "typescript": "^4.8.2" 18 | }, 19 | "scripts": { 20 | "clean": "tsc --build --clean", 21 | "build": "tsc --build", 22 | "watch": "tsc --build -w", 23 | "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js", 24 | "vlcn": "vlcn gen src/domain.vlcn -d src/domain && pnpm copy", 25 | "copy": "cp src/domain/generated/*.sql lib/domain/generated", 26 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 27 | }, 28 | "jest": { 29 | "testMatch": [ 30 | "**/__tests__/**/*.test.js" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ts/integration-tests/data-model/src/domain.vlcn: -------------------------------------------------------------------------------- 1 | engine: sqlite 2 | db: test 3 | 4 | User as Node { 5 | 1 id: ID 6 | 2 name: NaturalLanguage # & UserGenerated -- fields filled by users 7 | 3 created: Timestamp # & Computed & Immutable 8 | 4 modified: Timestamp 9 | } & OutboundEdges { 10 | # decks: Edge 11 | } -------------------------------------------------------------------------------- /ts/integration-tests/data-model/src/domain/User.ts: -------------------------------------------------------------------------------- 1 | import spec from "./generated/UserSpec.js"; 2 | import UserBase from "./generated/UserBase.js"; 3 | export { Data } from "./generated/UserBase.js"; 4 | 5 | export default class User extends UserBase { 6 | static readonly spec = spec; 7 | // insert any manual method you may have here 8 | } 9 | -------------------------------------------------------------------------------- /ts/integration-tests/data-model/src/domain/generated/User.sqlite.sql: -------------------------------------------------------------------------------- 1 | -- SIGNED-SOURCE: <49bb24eb9c8827bd5a8eafe30329090c> 2 | -- STATEMENT 3 | CREATE TABLE 4 | "user" ( 5 | "id" 6 | /* n=1 */ 7 | , 8 | "name" 9 | /* n=2 */ 10 | , 11 | "created" 12 | /* n=3 */ 13 | , 14 | "modified" 15 | /* n=4 */ 16 | , 17 | PRIMARY KEY ("id") 18 | ); -------------------------------------------------------------------------------- /ts/integration-tests/data-model/src/domain/generated/UserBase.ts: -------------------------------------------------------------------------------- 1 | // SIGNED-SOURCE: <23a5b5be739eda7ec6413bbca9e02d02> 2 | /** 3 | * AUTO-GENERATED FILE 4 | * Do not modify. Update your schema and re-generate for changes. 5 | */ 6 | import User from "../User.js"; 7 | import { default as s } from "./UserSpec.js"; 8 | import { P } from "@vlcn.io/runtime"; 9 | import { modelGenMemo } from "@vlcn.io/runtime"; 10 | import { AsyncPersistedModel } from "@vlcn.io/runtime"; 11 | import { INode } from "@vlcn.io/runtime"; 12 | import { NodeSpecWithCreate } from "@vlcn.io/runtime"; 13 | import { ID_of } from "@vlcn.io/runtime"; 14 | import UserQuery from "./UserQuery.js"; 15 | 16 | export type Data = { 17 | id: ID_of; 18 | name: string; 19 | created: number; 20 | modified: number; 21 | }; 22 | 23 | // @Sealed(User) 24 | export default abstract class UserBase 25 | extends AsyncPersistedModel 26 | implements INode 27 | { 28 | readonly spec = s as unknown as NodeSpecWithCreate; 29 | 30 | get id(): ID_of { 31 | return this.data.id as unknown as ID_of; 32 | } 33 | 34 | get name(): string { 35 | return this.data.name; 36 | } 37 | 38 | get created(): number { 39 | return this.data.created; 40 | } 41 | 42 | get modified(): number { 43 | return this.data.modified; 44 | } 45 | 46 | static queryAll(): UserQuery { 47 | return UserQuery.create(); 48 | } 49 | 50 | static genx = modelGenMemo( 51 | "test", 52 | "user", 53 | (id: ID_of): Promise => 54 | this.queryAll().whereId(P.equals(id)).genxOnlyValue() 55 | ); 56 | 57 | static gen = modelGenMemo( 58 | "test", 59 | "user", 60 | (id: ID_of): Promise => 61 | this.queryAll().whereId(P.equals(id)).genOnlyValue() 62 | ); 63 | 64 | static create(data: Data) { 65 | return User.spec.create(data); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ts/integration-tests/data-model/src/domain/generated/UserSpec.ts: -------------------------------------------------------------------------------- 1 | // SIGNED-SOURCE: 2 | /** 3 | * AUTO-GENERATED FILE 4 | * Do not modify. Update your schema and re-generate for changes. 5 | */ 6 | import { PersistedModel } from "@vlcn.io/runtime"; 7 | import { AsyncPersistedModel } from "@vlcn.io/runtime"; 8 | import { ID_of } from "@vlcn.io/runtime"; 9 | import { NodeSpecWithCreate } from "@vlcn.io/runtime"; 10 | import User from "../User.js"; 11 | import { Data } from "./UserBase.js"; 12 | 13 | const fields = { 14 | id: { 15 | encoding: "none", 16 | }, 17 | name: { 18 | encoding: "none", 19 | }, 20 | created: { 21 | encoding: "none", 22 | }, 23 | modified: { 24 | encoding: "none", 25 | }, 26 | } as const; 27 | const UserSpec: NodeSpecWithCreate = { 28 | type: "node", 29 | 30 | hydrate(data: Data) { 31 | return PersistedModel.hydrate(User, data); 32 | }, 33 | 34 | create(data: Data) { 35 | return AsyncPersistedModel.createOrUpdate(User, data); 36 | }, 37 | 38 | primaryKey: "id", 39 | 40 | storage: { 41 | engine: "sqlite", 42 | db: "test", 43 | type: "sql", 44 | tablish: "user", 45 | }, 46 | 47 | fields, 48 | 49 | outboundEdges: {}, 50 | }; 51 | 52 | export default UserSpec; 53 | -------------------------------------------------------------------------------- /ts/integration-tests/data-model/src/domain/generated/exports-node-sql.ts: -------------------------------------------------------------------------------- 1 | // SIGNED-SOURCE: <2d8752aaab3cb63b28bfcea3df8d1b00> 2 | /** 3 | * AUTO-GENERATED FILE 4 | * Do not modify. Update your schema and re-generate for changes. 5 | */ 6 | 7 | // @ts-ignore 8 | import * as path from "path"; 9 | // @ts-ignore 10 | import * as fs from "fs"; 11 | 12 | // @ts-ignore 13 | import { fileURLToPath } from "url"; 14 | 15 | const __filename = fileURLToPath(import.meta.url); 16 | const __dirname = path.dirname(__filename); 17 | 18 | const [User] = await Promise.all([ 19 | fs.promises.readFile(path.join(__dirname, "User.sqlite.sql"), { 20 | encoding: "utf8", 21 | }), 22 | ]); 23 | 24 | export default { 25 | sqlite: { 26 | test: { 27 | User, 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /ts/integration-tests/data-model/src/domain/generated/exports-sql.ts: -------------------------------------------------------------------------------- 1 | // SIGNED-SOURCE: 2 | /** 3 | * AUTO-GENERATED FILE 4 | * Do not modify. Update your schema and re-generate for changes. 5 | */ 6 | import User from "./User.sqlite.sql?raw"; 7 | export default { 8 | sqlite: { 9 | test: { 10 | User, 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /ts/integration-tests/data-model/src/domain/generated/exports.ts: -------------------------------------------------------------------------------- 1 | // SIGNED-SOURCE: <1c6e2363775cb1812578ec35d4cdfd47> 2 | /** 3 | * AUTO-GENERATED FILE 4 | * Do not modify. Update your schema and re-generate for changes. 5 | */ 6 | export { default as User } from "../User.js"; 7 | export { default as UserSpec } from "./UserSpec.js"; 8 | export { default as UserQuery } from "./UserQuery.js"; 9 | -------------------------------------------------------------------------------- /ts/integration-tests/data-model/src/domain/generated/types.d.ts: -------------------------------------------------------------------------------- 1 | // SIGNED-SOURCE: <7946820adce4295e4ffad7d91d8edf9a> 2 | /** 3 | * AUTO-GENERATED FILE 4 | * Do not modify. Update your schema and re-generate for changes. 5 | */ 6 | declare module "*.sql?raw"; 7 | -------------------------------------------------------------------------------- /ts/integration-tests/data-model/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [ 9 | { "path": "../../packages/runtime" }, 10 | { "path": "../../packages/codegen-cli" } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "version": "0.0.0", 4 | "packageManager": "pnpm@6.30.0", 5 | "engines": { 6 | "node": ">=16", 7 | "pnpm": ">=6" 8 | }, 9 | "devDependencies": { 10 | "@changesets/cli": "^2.24.3", 11 | "@tsconfig/node16": "^1.0.3", 12 | "turbo": "^1.4.3", 13 | "typescript": "^4.8.2", 14 | "leasot": "^13.2.0" 15 | }, 16 | "scripts": { 17 | "build": "cd build-all && pnpm build", 18 | "deep-clean": "turbo run deep-clean", 19 | "test": "turbo run test", 20 | "todos": "leasot '**/*.ts' --ignore '**/node_modules'", 21 | "preinstall": "npx only-allow pnpm" 22 | }, 23 | "dependencies": {} 24 | } 25 | -------------------------------------------------------------------------------- /ts/packages/cache/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /ts/packages/cache/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/packages/cache/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /ts/packages/cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/cache", 3 | "version": "0.3.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/cache" 10 | }, 11 | "dependencies": { 12 | "@vlcn.io/util": "workspace:*", 13 | "@vlcn.io/id": "workspace:*" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": "^7.18.13", 17 | "@babel/preset-env": "^7.18.10", 18 | "@types/jest": "^28.1.8", 19 | "fast-check": "^3.1.2", 20 | "jest": "^29.0.1", 21 | "typescript": "^4.8.2" 22 | }, 23 | "scripts": { 24 | "clean": "tsc --build --clean", 25 | "build": "tsc --build", 26 | "watch": "tsc --build -w", 27 | "test": "node --experimental-vm-modules --expose-gc --allow-natives-syntax ./node_modules/jest/bin/jest.js", 28 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 29 | }, 30 | "jest": { 31 | "testMatch": [ 32 | "**/__tests__/**/*.test.js" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ts/packages/cache/src/index.ts: -------------------------------------------------------------------------------- 1 | import Cache from './cache.js'; 2 | 3 | export default Cache; 4 | -------------------------------------------------------------------------------- /ts/packages/cache/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [{ "path": "../util" }, { "path": "../id" }] 9 | } 10 | -------------------------------------------------------------------------------- /ts/packages/codegen-api/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /ts/packages/codegen-api/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/packages/codegen-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/codegen-api", 3 | "version": "0.3.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/codegen-api" 10 | }, 11 | "dependencies": { 12 | "@vlcn.io/schema-api": "workspace:*", 13 | "md5": "^2.3.0" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": "^7.18.13", 17 | "@babel/preset-env": "^7.18.10", 18 | "@types/jest": "^28.1.8", 19 | "@types/node": "^18.7.13", 20 | "@types/prettier": "^2.7.0", 21 | "@typescript-eslint/typescript-estree": "^5.35.1", 22 | "fast-check": "^3.1.2", 23 | "jest": "^29.0.1", 24 | "typescript": "^4.8.2" 25 | }, 26 | "scripts": { 27 | "clean": "tsc --build --clean", 28 | "build": "tsc --build", 29 | "watch": "tsc --build -w", 30 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 31 | }, 32 | "jest": { 33 | "testMatch": [ 34 | "**/__tests__/**/*.test.js" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ts/packages/codegen-api/src/CodegenStep.ts: -------------------------------------------------------------------------------- 1 | import { CodegenFile } from './CodegenFile.js'; 2 | 3 | export default abstract class CodegenStep { 4 | constructor() {} 5 | 6 | abstract gen(): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /ts/packages/codegen-api/src/__tests__/uniqueImports.test.ts: -------------------------------------------------------------------------------- 1 | import uniqueImports from "../uniqueImports.js"; 2 | import { Import } from "@vlcn.io/schema-api"; 3 | import fc from "fast-check"; 4 | 5 | test("matching imports are de-duplicated", () => { 6 | fc.assert( 7 | fc.property( 8 | fc.string(), 9 | fc.string(), 10 | fc.string(), 11 | fc.integer({ min: 1, max: 10 }), 12 | (name, as, from, num) => { 13 | const imports: Import[] = []; 14 | for (let i = 0; i < num; ++i) { 15 | imports.push({ 16 | name, 17 | as, 18 | from, 19 | }); 20 | } 21 | const uniqued = uniqueImports(imports); 22 | expect(uniqued.length).toEqual(1); 23 | } 24 | ) 25 | ); 26 | }); 27 | 28 | test("mismatching imports are not de-duplicated", () => { 29 | fc.assert( 30 | fc.property( 31 | fc.string(), 32 | fc.string(), 33 | fc.string(), 34 | fc.integer({ min: 1, max: 10 }), 35 | (name, as, from, num) => { 36 | const imports: Import[] = []; 37 | for (let i = 0; i < num; ++i) { 38 | imports.push({ 39 | name: `${name}-${i}`, 40 | as, 41 | from, 42 | }); 43 | } 44 | const uniqued = uniqueImports(imports); 45 | expect(uniqued.length).toEqual(imports.length); 46 | } 47 | ) 48 | ); 49 | }); 50 | -------------------------------------------------------------------------------- /ts/packages/codegen-api/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./CodegenFile.js"; 2 | export { default as CodegenStep } from "./CodegenStep.js"; 3 | export { default as CodegenPipeline } from "./CodegenPipeline.js"; 4 | export { default as uniqueImports } from "./uniqueImports.js"; 5 | 6 | import CodegenStep from "./CodegenStep.js"; 7 | import { SchemaNode, SchemaEdge, SchemaFile } from "@vlcn.io/schema-api"; 8 | 9 | export type Step = { 10 | new (opts: { 11 | nodeOrEdge: SchemaNode | SchemaEdge; 12 | edges: { [key: string]: SchemaEdge }; 13 | dest: string; 14 | }): CodegenStep; 15 | accepts: (x: SchemaNode | SchemaEdge) => boolean; 16 | }; 17 | 18 | export type GlobalStep = { 19 | new (nodes: SchemaNode[], edges: SchemaEdge[], dest: string): CodegenStep; 20 | accepts: (nodes: SchemaNode[], edges: SchemaEdge[]) => boolean; 21 | }; 22 | 23 | export const generatedDir = "generated"; 24 | -------------------------------------------------------------------------------- /ts/packages/codegen-api/src/uniqueImports.ts: -------------------------------------------------------------------------------- 1 | import { Import } from "@vlcn.io/schema-api"; 2 | 3 | export default function uniqueImports(imports: readonly Import[]): Import[] { 4 | const seen = new Set(); 5 | const ret = imports.filter((i) => { 6 | const key = toKey(i); 7 | if (seen.has(key)) { 8 | return false; 9 | } 10 | 11 | seen.add(key); 12 | return true; 13 | }); 14 | 15 | return ret; 16 | } 17 | 18 | function toKey(i: Import) { 19 | return i.name + "-" + i.as + "-" + i.from; 20 | } 21 | -------------------------------------------------------------------------------- /ts/packages/codegen-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [{ "path": "../schema-api" }] 9 | } 10 | -------------------------------------------------------------------------------- /ts/packages/codegen-cli/.gitignore: -------------------------------------------------------------------------------- 1 | bin/ -------------------------------------------------------------------------------- /ts/packages/codegen-cli/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/packages/codegen-cli/README.md: -------------------------------------------------------------------------------- 1 | # Codegen CLI 2 | 3 | The command line interface for running the codegen commands. 4 | 5 | Currently includes and executes core extensions. -------------------------------------------------------------------------------- /ts/packages/codegen-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/codegen-cli", 3 | "version": "0.4.0", 4 | "main": "bin/cli.js", 5 | "type": "module", 6 | "bin": { 7 | "vlcn": "./bin/cli.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/vulcan-sh/vulcan.git", 12 | "directory": "packages/codegen-cli" 13 | }, 14 | "dependencies": { 15 | "@vlcn.io/codegen-ts": "workspace:*", 16 | "@vlcn.io/codegen-api": "workspace:*", 17 | "@vlcn.io/codegen-sql": "workspace:*", 18 | "@vlcn.io/schema": "workspace:*", 19 | "@vlcn.io/schema-api": "workspace:*", 20 | "@vlcn.io/util": "workspace:*", 21 | "@strut/counter": "^0.0.11", 22 | "chalk": "^5.0.1", 23 | "command-line-args": "^5.2.1", 24 | "command-line-usage": "^6.1.3", 25 | "md5": "^2.3.0", 26 | "prettier": "^2.7.1" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.18.13", 30 | "@babel/preset-env": "^7.18.10", 31 | "@types/jest": "^28.1.8", 32 | "@types/node": "^18.7.13", 33 | "@types/prettier": "^2.7.0", 34 | "@typescript-eslint/typescript-estree": "^5.35.1", 35 | "jest": "^29.0.1", 36 | "typescript": "^4.8.2" 37 | }, 38 | "scripts": { 39 | "clean": "tsc --build --clean", 40 | "build": "tsc --build", 41 | "watch": "tsc --build -w", 42 | "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js", 43 | "deep-clean": "rm -rf ./bin || true && rm tsconfig.tsbuildinfo || true" 44 | }, 45 | "jest": { 46 | "testMatch": [ 47 | "**/__tests__/**/*.test.js" 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ts/packages/codegen-cli/src/__tests__/cli.test.ts: -------------------------------------------------------------------------------- 1 | test('TEST TODO', () => {}); 2 | -------------------------------------------------------------------------------- /ts/packages/codegen-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./bin/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [ 9 | { "path": "../codegen-api" }, 10 | { "path": "../schema" }, 11 | { "path": "../schema-api" }, 12 | { "path": "../codegen-ts" }, 13 | { "path": "../codegen-sql" } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /ts/packages/codegen-sql/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /ts/packages/codegen-sql/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/packages/codegen-sql/README.md: -------------------------------------------------------------------------------- 1 | # Codegen SQL 2 | 3 | Code generators for all SQL dialects. These generators generate table definitions, not queries, given query definitions are crafted at runtime. -------------------------------------------------------------------------------- /ts/packages/codegen-sql/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /ts/packages/codegen-sql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/codegen-sql", 3 | "version": "0.3.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/codegen-sql" 10 | }, 11 | "dependencies": { 12 | "@vlcn.io/codegen-api": "workspace:*", 13 | "@vlcn.io/schema": "workspace:*", 14 | "@vlcn.io/schema-api": "workspace:*", 15 | "@vlcn.io/sql": "workspace:*", 16 | "@vlcn.io/util": "workspace:*", 17 | "sql-formatter": "10.0.0" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.18.13", 21 | "@babel/preset-env": "^7.18.10", 22 | "@types/jest": "^28.1.8", 23 | "@types/prettier": "^2.7.0", 24 | "@typescript-eslint/typescript-estree": "^5.35.1", 25 | "@types/node": "^18.7.13", 26 | "jest": "^29.0.1", 27 | "typescript": "^4.8.2" 28 | }, 29 | "scripts": { 30 | "clean": "tsc --build --clean", 31 | "build": "tsc --build", 32 | "watch": "tsc --build -w", 33 | "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js", 34 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 35 | }, 36 | "jest": { 37 | "testMatch": [ 38 | "**/__tests__/**/*.test.js" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ts/packages/codegen-sql/src/SqlFile.ts: -------------------------------------------------------------------------------- 1 | import { CodegenFile, sqlTemplates, sign } from "@vlcn.io/codegen-api"; 2 | import { format } from "sql-formatter"; 3 | 4 | export default class SqlFile implements CodegenFile { 5 | #contents: string; 6 | readonly templates = sqlTemplates; 7 | 8 | constructor( 9 | public readonly name: string, 10 | contents: string, 11 | private dialect: string, 12 | public readonly isUnsigned: boolean = false 13 | ) { 14 | this.#contents = contents; 15 | } 16 | 17 | get contents(): string { 18 | return sign( 19 | format(this.#contents, { language: this.#dialectToLanguage() as any }), 20 | this.templates 21 | ); 22 | } 23 | 24 | #dialectToLanguage() { 25 | if (this.dialect === "postgres") { 26 | return "postgresql"; 27 | } 28 | 29 | return this.dialect; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ts/packages/codegen-sql/src/__tests__/GenSqlTableSchema.test.ts: -------------------------------------------------------------------------------- 1 | import { createCompiler } from "@vlcn.io/schema"; 2 | import { SchemaNode } from "@vlcn.io/schema-api"; 3 | import GenSqlTableSchema from "../GenSqlTableSchema"; 4 | 5 | const { compileFromString } = createCompiler(); 6 | 7 | const basicSchema = ` 8 | engine: sqlite 9 | db: test 10 | 11 | Foo as Node { 12 | id: ID 13 | i32: int32 14 | ui32: uint32 15 | i64: int64 16 | ui64: uint64 17 | f32: float32 18 | f64: float64 19 | string: string 20 | bool: bool 21 | enum: Enumeration 22 | timestamp: Timestamp 23 | lang: NaturalLanguage 24 | }`; 25 | 26 | test("sqlite & basic schema", async () => { 27 | const schema = compileIt(basicSchema); 28 | const file = await genIt(schema); 29 | expect(file.contents) 30 | .toEqual(`-- SIGNED-SOURCE: <0e6c49915e3079d3e14291d4fdf5e542> 31 | -- STATEMENT 32 | CREATE TABLE 33 | "foo" ( 34 | "id", 35 | "i32", 36 | "ui32", 37 | "i64", 38 | "ui64", 39 | "f32", 40 | "f64", 41 | "string", 42 | "bool", 43 | "enum", 44 | "timestamp", 45 | "lang", 46 | PRIMARY KEY ("id") 47 | );`); 48 | }); 49 | 50 | async function genIt(schema: SchemaNode) { 51 | return await new GenSqlTableSchema({ 52 | nodeOrEdge: schema, 53 | edges: {}, 54 | dest: "", 55 | }).gen(); 56 | } 57 | 58 | function compileIt(schema: string) { 59 | return compileFromString(schema)[1].nodes.Foo; 60 | } 61 | -------------------------------------------------------------------------------- /ts/packages/codegen-sql/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as GenSqlTableSchema } from './GenSqlTableSchema.js'; 2 | -------------------------------------------------------------------------------- /ts/packages/codegen-sql/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [ 9 | { "path": "../codegen-api" }, 10 | { "path": "../schema" }, 11 | { "path": "../schema-api" }, 12 | { "path": "../sql" }, 13 | { "path": "../util" } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /ts/packages/codegen-ts/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /ts/packages/codegen-ts/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/packages/codegen-ts/README.md: -------------------------------------------------------------------------------- 1 | # Codegen TS 2 | 3 | The core generators for TypeScript. 4 | - Model 5 | - Spec 6 | - Query 7 | 8 | Mutators, Authz, GraphQL, etc. codegen are supplied via extensions. -------------------------------------------------------------------------------- /ts/packages/codegen-ts/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /ts/packages/codegen-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/codegen-ts", 3 | "version": "0.5.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/codegen-ts" 10 | }, 11 | "dependencies": { 12 | "@vlcn.io/codegen-api": "workspace:*", 13 | "@vlcn.io/schema": "workspace:*", 14 | "@vlcn.io/schema-api": "workspace:*", 15 | "@vlcn.io/feature-gates": "workspace:*", 16 | "@vlcn.io/util": "workspace:*", 17 | "md5": "^2.3.0", 18 | "prettier": "^2.7.1" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.18.13", 22 | "@babel/preset-env": "^7.18.10", 23 | "@types/jest": "^28.1.8", 24 | "@types/node": "^18.7.13", 25 | "@types/prettier": "^2.7.0", 26 | "@typescript-eslint/typescript-estree": "^5.35.1", 27 | "jest": "^29.0.1", 28 | "typescript": "^4.8.2" 29 | }, 30 | "scripts": { 31 | "clean": "tsc --build --clean", 32 | "build": "tsc --build", 33 | "watch": "tsc --build -w", 34 | "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js", 35 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 36 | }, 37 | "jest": { 38 | "testMatch": [ 39 | "**/__tests__/**/*.test.js" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ts/packages/codegen-ts/src/GenSQLExports_node.ts: -------------------------------------------------------------------------------- 1 | import { SchemaEdge, SchemaNode } from "@vlcn.io/schema-api"; 2 | import { GenSQLExports } from "./GenSQLExports.js"; 3 | 4 | export class GenSQLExports_node extends GenSQLExports { 5 | protected getImports() { 6 | // The user might not want to pull node types into their project. Makes sense if it is a browser project. 7 | // So ts-ignore these. 8 | return ` 9 | // @ts-ignore 10 | import * as path from 'path'; 11 | // @ts-ignore 12 | import * as fs from 'fs'; 13 | 14 | // @ts-ignore 15 | import { fileURLToPath } from 'url'; 16 | 17 | const __filename = fileURLToPath(import.meta.url); 18 | const __dirname = path.dirname(__filename); 19 | 20 | const [${this.all.map((nore) => nore.name).join(",\n")}] = await Promise.all([ 21 | ${this.all.map(this.getReadFileCode).join(",\n")} 22 | ]); 23 | `; 24 | } 25 | 26 | protected getFilename() { 27 | return "exports-node-sql.ts"; 28 | } 29 | 30 | private getReadFileCode(nore: SchemaNode | SchemaEdge): string { 31 | return `fs.promises.readFile(path.join(__dirname, '${nore.name}.${nore.storage.engine}.sql'), {encoding: "utf8"})`; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ts/packages/codegen-ts/src/GenSchemaExports.ts: -------------------------------------------------------------------------------- 1 | import { CodegenStep, CodegenFile, generatedDir } from "@vlcn.io/codegen-api"; 2 | import { nodeFn } from "@vlcn.io/schema"; 3 | import { SchemaEdge, SchemaNode } from "@vlcn.io/schema-api"; 4 | import * as path from "path"; 5 | import GenTypescriptQuery from "./GenTypescriptQuery.js"; 6 | import GenTypescriptSpec from "./GenTypescriptSpec.js"; 7 | import TypescriptFile from "./TypescriptFile.js"; 8 | 9 | export class GenSchemaExports extends CodegenStep { 10 | constructor( 11 | private nodes: SchemaNode[], 12 | private edges: SchemaEdge[], 13 | private schemaFileName: string 14 | ) { 15 | super(); 16 | } 17 | 18 | static accepts(nodes: SchemaNode[], edges: SchemaEdge[]): boolean { 19 | return true; 20 | } 21 | 22 | async gen(): Promise { 23 | const filename = "exports.ts"; 24 | const code = `${this.nodes.map(this.getExportCode).join("\n")} 25 | ${this.edges.map(this.getExportCode).join("\n")}`; 26 | return new TypescriptFile(path.join(generatedDir, filename), code); 27 | } 28 | 29 | private getExportCode(nodeOrEdge: SchemaEdge | SchemaNode): string { 30 | const exports = [ 31 | `export { default as ${nodeOrEdge.name} } from "../${nodeOrEdge.name}.js";`, 32 | ]; 33 | if ((nodeOrEdge.extensions as any).mutations) { 34 | exports.push( 35 | `export { default as ${nodeOrEdge.name}Mutations } from "./${nodeOrEdge.name}Mutations.js";` 36 | ); 37 | } 38 | 39 | if (GenTypescriptSpec.accepts(nodeOrEdge)) { 40 | exports.push( 41 | `export { default as ${nodeFn.specName( 42 | nodeOrEdge.name 43 | )} } from "./${nodeFn.specName(nodeOrEdge.name)}.js"` 44 | ); 45 | } 46 | if (GenTypescriptQuery.accepts(nodeOrEdge)) { 47 | exports.push( 48 | `export { default as ${nodeFn.queryTypeName( 49 | nodeOrEdge.name 50 | )} } from "./${nodeFn.queryTypeName(nodeOrEdge.name)}.js"` 51 | ); 52 | } 53 | 54 | return exports.join("\n"); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ts/packages/codegen-ts/src/GenTypes_d_ts.ts: -------------------------------------------------------------------------------- 1 | // declare module "*.sql?raw"; 2 | import { CodegenStep, CodegenFile, generatedDir } from "@vlcn.io/codegen-api"; 3 | import { SchemaEdge, SchemaNode } from "@vlcn.io/schema-api"; 4 | import * as path from "path"; 5 | import TypescriptFile from "./TypescriptFile.js"; 6 | 7 | export class GenTypes_d_ts extends CodegenStep { 8 | constructor( 9 | private nodes: SchemaNode[], 10 | private edges: SchemaEdge[], 11 | private schemaFileName: string 12 | ) { 13 | super(); 14 | } 15 | 16 | static accepts(nodes: SchemaNode[], edges: SchemaEdge[]): boolean { 17 | return true; 18 | } 19 | 20 | async gen(): Promise { 21 | const filename = "types.d.ts"; 22 | return new TypescriptFile( 23 | path.join(generatedDir, filename), 24 | `declare module "*.sql?raw";` 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ts/packages/codegen-ts/src/GenTypescriptModelManualMethodsClass.ts: -------------------------------------------------------------------------------- 1 | import { CodegenFile, CodegenStep, generatedDir } from "@vlcn.io/codegen-api"; 2 | import { SchemaEdge, SchemaNode } from "@vlcn.io/schema-api"; 3 | import TypescriptFile from "./TypescriptFile.js"; 4 | import * as fs from "fs"; 5 | import * as path from "path"; 6 | 7 | export default class GenTypescriptModelManualMethodsClass extends CodegenStep { 8 | static accepts(schema: SchemaNode | SchemaEdge): boolean { 9 | return true; 10 | } 11 | 12 | private readonly schema: SchemaNode | SchemaEdge; 13 | private edges: { [key: string]: SchemaEdge }; 14 | private dest: string; 15 | 16 | constructor(opts: { 17 | nodeOrEdge: SchemaNode | SchemaEdge; 18 | edges: { [key: string]: SchemaEdge }; 19 | dest: string; 20 | }) { 21 | super(); 22 | this.schema = opts.nodeOrEdge; 23 | this.edges = opts.edges; 24 | this.dest = opts.dest; 25 | } 26 | 27 | async gen(): Promise { 28 | const filename = this.schema.name + ".ts"; 29 | let exists = false; 30 | try { 31 | await fs.promises.access(path.join(this.dest, filename)); 32 | exists = true; 33 | } catch (e) {} 34 | if (exists) { 35 | return new TypescriptFile("", "", true, true); 36 | } 37 | 38 | return new TypescriptFile( 39 | filename, 40 | `import spec from './${generatedDir}/${this.schema.name}Spec.js'; 41 | import ${this.schema.name}Base from './${generatedDir}/${this.schema.name}Base.js'; 42 | export {Data} from './${generatedDir}/${this.schema.name}Base.js'; 43 | 44 | export default class ${this.schema.name} extends ${this.schema.name}Base { 45 | static readonly spec = spec; 46 | // insert any manual method you may have here 47 | } 48 | `, 49 | true 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ts/packages/codegen-ts/src/TypescriptFile.ts: -------------------------------------------------------------------------------- 1 | import { sign } from "@vlcn.io/codegen-api"; 2 | import { CodegenFile, algolTemplates } from "@vlcn.io/codegen-api"; 3 | // @ts-ignore 4 | import prettier from "prettier"; 5 | 6 | export default class TypescriptFile implements CodegenFile { 7 | #contents: string; 8 | readonly templates = algolTemplates; 9 | 10 | constructor( 11 | public readonly name: string, 12 | contents: string, 13 | public readonly isUnsigned: boolean = false, 14 | public readonly nochange: boolean = false 15 | ) { 16 | this.#contents = contents; 17 | } 18 | 19 | get contents(): string { 20 | let contents = this.#contents; 21 | if (this.isUnsigned) { 22 | return prettier.format(contents, { parser: "typescript" }); 23 | } 24 | 25 | contents = 26 | `/** 27 | * AUTO-GENERATED FILE 28 | * Do not modify. Update your schema and re-generate for changes. 29 | */ 30 | ` + this.#contents; 31 | 32 | return sign( 33 | prettier.format(contents, { parser: "typescript" }), 34 | this.templates 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ts/packages/codegen-ts/src/__tests__/GenTypesriptSpec.test.ts: -------------------------------------------------------------------------------- 1 | test('write some tests dude', () => {}); 2 | -------------------------------------------------------------------------------- /ts/packages/codegen-ts/src/__tests__/tsUtils.test.ts: -------------------------------------------------------------------------------- 1 | import { fieldToTsType } from '../tsUtils'; 2 | 3 | test('field to ts type', () => { 4 | expect( 5 | fieldToTsType({ 6 | type: ['null'], 7 | }), 8 | ).toEqual('null'); 9 | 10 | expect( 11 | fieldToTsType({ 12 | type: [ 13 | { 14 | type: 'primitive', 15 | subtype: 'null', 16 | }, 17 | ], 18 | }), 19 | ).toEqual('null'); 20 | 21 | expect( 22 | fieldToTsType({ 23 | type: ['Foo'], 24 | }), 25 | ).toEqual('Foo'); 26 | }); 27 | -------------------------------------------------------------------------------- /ts/packages/codegen-ts/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as GenTypescriptModel } from './GenTypescriptModel.js'; 2 | export { default as GenTypescriptQuery } from './GenTypescriptQuery.js'; 3 | export { default as GenTypescriptSpec } from './GenTypescriptSpec.js'; 4 | export { default as GenTypescriptModelManualMethodsClass } from './GenTypescriptModelManualMethodsClass.js'; 5 | export * from './tsUtils.js'; 6 | export { default as TypescriptFile } from './TypescriptFile.js'; 7 | export * from './GenSchemaExports.js'; 8 | export * from './GenSQLExports.js'; 9 | export * from './GenTypes_d_ts.js'; 10 | export * from './GenSQLExports_node.js'; 11 | -------------------------------------------------------------------------------- /ts/packages/codegen-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src", 6 | "declarationMap": false 7 | }, 8 | "include": ["./src/"], 9 | "references": [ 10 | { "path": "../codegen-api" }, 11 | { "path": "../schema" }, 12 | { "path": "../schema-api" }, 13 | { "path": "../feature-gates" } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /ts/packages/config/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/packages/config/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @vlcn.io/config 2 | 3 | ## 0.3.0 4 | 5 | ### Minor Changes 6 | 7 | - publish for testing 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - @vlcn.io/cache@0.3.0 13 | - @vlcn.io/sql@0.3.0 14 | -------------------------------------------------------------------------------- /ts/packages/config/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /ts/packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/config", 3 | "version": "0.3.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/config" 10 | }, 11 | "dependencies": { 12 | "@vlcn.io/sql": "workspace:*", 13 | "@vlcn.io/cache": "workspace:*" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": "^7.18.13", 17 | "@babel/preset-env": "^7.18.10", 18 | "@types/jest": "^28.1.8", 19 | "fast-check": "^3.1.2", 20 | "jest": "^29.0.1", 21 | "typescript": "^4.8.2" 22 | }, 23 | "scripts": { 24 | "clean": "tsc --build --clean", 25 | "build": "tsc --build", 26 | "watch": "tsc --build -w", 27 | "test": "node --experimental-vm-modules --expose-gc --allow-natives-syntax ./node_modules/jest/bin/jest.js", 28 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 29 | }, 30 | "jest": { 31 | "testMatch": [ 32 | "**/__tests__/**/*.test.js" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ts/packages/config/src/context.ts: -------------------------------------------------------------------------------- 1 | export type Context = {}; 2 | -------------------------------------------------------------------------------- /ts/packages/config/src/index.ts: -------------------------------------------------------------------------------- 1 | import vconfig from "./vulcan-config.js"; 2 | export * from "./vulcan-config.js"; 3 | 4 | export const config = vconfig.config; 5 | export const init = vconfig.init; 6 | -------------------------------------------------------------------------------- /ts/packages/config/src/vulcan-config.ts: -------------------------------------------------------------------------------- 1 | import Cache from "@vlcn.io/cache"; 2 | import { SQLQuery } from "@vlcn.io/sql"; 3 | 4 | export type StorageEngine = "ephemeral" | "memory" | "sqlite"; 5 | 6 | export type DBResolver = { 7 | storage(engine: StorageEngine, dbName: string): ResolvedDB; 8 | }; 9 | 10 | export type ResolvedDB = AsyncResolvedDB | SyncResolvedDB; 11 | export type AsyncResolvedDB = SQLResolvedDB; 12 | export type SyncResolvedDB = MemoryResolvedDB; 13 | 14 | export type MemoryReadQuery = { 15 | type: "read"; 16 | tablish: string; 17 | // undefined --> all 18 | // [] --> none 19 | // [id, ...] --> specific ids 20 | roots?: any[]; 21 | }; 22 | 23 | export type MemoryWriteQuery = { 24 | type: "write"; 25 | op: "delete" | "upsert"; 26 | tablish: string; 27 | models: { id: any }[]; 28 | }; 29 | 30 | export type MemoryQuery = MemoryReadQuery | MemoryWriteQuery; 31 | 32 | export type MemoryResolvedDB = { 33 | read(q: MemoryReadQuery): any[]; 34 | write(q: MemoryWriteQuery): void; 35 | dispose(): void; 36 | 37 | begin(): void; 38 | commit(): void; 39 | rollback(): void; 40 | }; 41 | 42 | export type SQLResolvedDB = { 43 | read(q: SQLQuery): Promise; 44 | write(q: SQLQuery): Promise; 45 | dispose(): void; 46 | 47 | begin(): Promise; 48 | commit(): Promise; 49 | rollback(): Promise; 50 | }; 51 | 52 | export type Config = { 53 | storage(engine: StorageEngine, dbName: string): ResolvedDB; 54 | readonly cache: Cache; 55 | }; 56 | 57 | let _config: Config | null = null; 58 | 59 | export default { 60 | init(config: Config) { 61 | if (_config != null) { 62 | if (_config != config) { 63 | throw new Error( 64 | "Attempt to reset config to a new value after it was already set" 65 | ); 66 | } 67 | return; 68 | } 69 | _config = config; 70 | }, 71 | 72 | get config(): Config { 73 | return _config!; 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /ts/packages/config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [] 9 | } 10 | -------------------------------------------------------------------------------- /ts/packages/feature-gates/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /ts/packages/feature-gates/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/packages/feature-gates/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @vlcn.io/feature-gates 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - publish for testing 8 | -------------------------------------------------------------------------------- /ts/packages/feature-gates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/feature-gates", 3 | "version": "0.1.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/feature-gates" 10 | }, 11 | "devDependencies": { 12 | "typescript": "^4.8.2" 13 | }, 14 | "scripts": { 15 | "clean": "tsc --build --clean", 16 | "build": "tsc --build", 17 | "watch": "tsc --build -w", 18 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 19 | }, 20 | "jest": { 21 | "testMatch": [ 22 | "**/__tests__/**/*.test.js" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ts/packages/feature-gates/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Features we've decided to disable or enable in the current build of Aphrodite 3 | * 4 | * This was introduced as a way to strip down Aphrodite to a less complex beast 5 | * by disabling non essential features. 6 | */ 7 | export default { 8 | NAMED_MUTATIONS: false, 9 | }; 10 | -------------------------------------------------------------------------------- /ts/packages/feature-gates/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"] 8 | } 9 | -------------------------------------------------------------------------------- /ts/packages/grammar-extension-api/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /ts/packages/grammar-extension-api/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/packages/grammar-extension-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/grammar-extension-api", 3 | "version": "0.3.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "packages/grammar-extension-api" 10 | }, 11 | "dependencies": { 12 | "@vlcn.io/schema-api": "workspace:*", 13 | "ohm-js": "^16.4.0" 14 | }, 15 | "devDependencies": { 16 | "typescript": "^4.8.2" 17 | }, 18 | "scripts": { 19 | "clean": "tsc --build --clean", 20 | "build": "tsc --build", 21 | "watch": "tsc --build -w", 22 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 23 | }, 24 | "jest": { 25 | "testMatch": [ 26 | "**/__tests__/**/*.test.js" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ts/packages/grammar-extension-api/src/index.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from "@vlcn.io/schema-api"; 2 | import { ActionDict } from "ohm-js"; 3 | 4 | type RuleName = string; 5 | interface ExtensionPoints { 6 | NodeFunction?: RuleName; 7 | EdgeFunction?: RuleName; 8 | // eventually fields too 9 | // and... extensions of extensions? e.g., auth on mutations... 10 | 11 | // should we just invert control instead? 12 | // make a pipeline where each stage of the pipeline augments 13 | // the parser... 14 | // 15 | // and provide utils to do so. 16 | // 17 | // this'll make extensions of extensions easier to implement. 18 | // or... we can just do it this way and one can register an 19 | // extension point when they add their extension... 20 | // 21 | // declare module, interface extension trick... 22 | } 23 | 24 | export interface GrammarExtension { 25 | readonly name: string; //Symbol; 26 | readonly extends: ExtensionPoints; 27 | 28 | grammar(): string; 29 | actions(): ActionDict; 30 | condensor(ast: TAst): [ValidationError[], TCondensed]; 31 | } 32 | -------------------------------------------------------------------------------- /ts/packages/grammar-extension-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [{ "path": "../schema-api" }] 9 | } 10 | -------------------------------------------------------------------------------- /ts/packages/id/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @vlcn.io/id 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - publish for testing 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - @vlcn.io/util@0.1.0 13 | 14 | ## 0.0.2 15 | 16 | ### Patch Changes 17 | 18 | - retryable transactions, serializable transactions, conflict detection 19 | - Updated dependencies 20 | - @vlcn.io/util@0.0.2 21 | -------------------------------------------------------------------------------- /ts/packages/id/README.md: -------------------------------------------------------------------------------- 1 | # vulcan.sh/id 2 | 3 | `UUIDs` aren't great when it comes to using them as keys in a database. This has been covered in numerous blog posts. These two do a good job of explaining the problems: 4 | 5 | - https://www.percona.com/blog/2019/11/22/uuids-are-popular-but-bad-for-performance-lets-discuss/ 6 | - https://thenewobjective.com/software-systems-engineering/sql-server-and-uuids 7 | 8 | There's a new UUID format proposed to fix this issue: 9 | https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format 10 | 11 | While this package does not implement that format it implements a derivation of [uuid_short](https://mariadb.com/kb/en/uuid_short/) proposed at the bottom of [this article](https://www.percona.com/blog/2019/11/22/uuids-are-popular-but-bad-for-performance-lets-discuss/). 12 | 13 | # What again? 14 | 15 | tldr: this package generates 64bit integer ids which, between a bounded set of devices, will be unique. 16 | 17 | Proof please? 18 | -------------------------------------------------------------------------------- /ts/packages/id/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /ts/packages/id/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/id", 3 | "version": "0.1.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/id" 10 | }, 11 | "dependencies": { 12 | "@vlcn.io/util": "workspace:*" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.18.13", 16 | "@babel/preset-env": "^7.18.10", 17 | "@types/jest": "^28.1.8", 18 | "fast-check": "^3.1.2", 19 | "jest": "^29.0.1", 20 | "typescript": "^4.8.2" 21 | }, 22 | "scripts": { 23 | "clean": "tsc --build --clean", 24 | "build": "tsc --build", 25 | "watch": "tsc --build -w", 26 | "test": "node ./node_modules/jest/bin/jest.js", 27 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 28 | }, 29 | "jest": { 30 | "testMatch": [ 31 | "**/__tests__/**/*.test.js" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ts/packages/id/src/__tests__/id.test.ts: -------------------------------------------------------------------------------- 1 | import { isHex } from "@vlcn.io/util"; 2 | import { newId } from "../id.js"; 3 | 4 | test("Requires hex device id", () => { 5 | expect(() => newId("zyx")).toThrow(); 6 | expect(() => newId("01AF")).not.toThrow(); 7 | }); 8 | 9 | test("Returned value is hex", () => { 10 | const id = newId("01AF", "hex"); 11 | expect(isHex(id)).toBe(true); 12 | }); 13 | 14 | test("Returned value is 64bits (8 bytes)", () => { 15 | const id = newId("F1AF", "hex"); 16 | expect(id.length).toBe(16); 17 | }); 18 | 19 | test("Device ids are required to be 2 bytes or more", () => { 20 | expect(() => newId("1")).toThrow(); 21 | expect(() => newId("12")).toThrow(); 22 | expect(() => newId("123")).toThrow(); 23 | expect(() => newId("1234")).not.toThrow(); 24 | }); 25 | 26 | test("sids can be decimal or hex", () => { 27 | const decimal = newId("01AF", "decimal"); 28 | const hex = newId("01AF", "hex"); 29 | 30 | expect(BigInt("0x" + hex)).toEqual(BigInt(decimal) + 1n); 31 | }); 32 | -------------------------------------------------------------------------------- /ts/packages/id/src/id.ts: -------------------------------------------------------------------------------- 1 | export type DeviceId = string; 2 | 3 | import { invariant, assertUnreachable } from "@vlcn.io/util"; 4 | 5 | // 32 bit random var in decimal 6 | let randomVariable = Math.floor(Number.MAX_SAFE_INTEGER * Math.random()); 7 | 8 | export function newId( 9 | deviceId: DeviceId, 10 | base: "hex" | "decimal" = "hex" 11 | ): ID_of { 12 | invariant(isHex(deviceId), "Device ID must be a hex string"); 13 | invariant(deviceId.length >= 4, "Device ids must be at least 2 bytes"); 14 | 15 | // 32 bits, hex 16 | const hi32 = Math.floor(Date.now() / 1000).toString(16); 17 | 18 | // low 16 bits of device, in hex 19 | const partialDevice = deviceId.substring(deviceId.length - 4); 20 | // low 16 bits of the random variable, in hex 21 | const random = (++randomVariable & 0xffff).toString(16); 22 | 23 | const low32 = partialDevice + random; 24 | const hex = (hi32 + low32) as ID_of; 25 | 26 | if (base === "hex") { 27 | return hex; 28 | } 29 | 30 | if (base === "decimal") { 31 | return BigInt("0x" + hex).toString() as ID_of; 32 | } 33 | 34 | assertUnreachable(base); 35 | } 36 | 37 | export function asId(id: string): ID_of { 38 | return id as ID_of; 39 | } 40 | 41 | export function truncateForDisplay(id: string) { 42 | return id.substring(id.length - 6); 43 | } 44 | 45 | const hexReg = /^[0-9A-Fa-f]+$/; 46 | function isHex(h: string) { 47 | return hexReg.exec(h) != null; 48 | } 49 | 50 | // https://github.com/seancroach/ts-opaque 51 | 52 | export type Opaque = BaseType & { 53 | readonly [Symbols.base]: BaseType; 54 | readonly [Symbols.brand]: BrandType; 55 | }; 56 | 57 | namespace Symbols { 58 | export declare const base: unique symbol; 59 | export declare const brand: unique symbol; 60 | } 61 | 62 | export type ID_of = Opaque; 63 | -------------------------------------------------------------------------------- /ts/packages/id/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./id.js"; 2 | export * from "./uuidv7.js"; 3 | -------------------------------------------------------------------------------- /ts/packages/id/src/uuidv7.ts: -------------------------------------------------------------------------------- 1 | // from https://github.com/kripod/uuidv7 but without the dashes for easier storage as 16 byte primary key 2 | 3 | const UNIX_TS_MS_BITS = 48; 4 | const VER_DIGIT = "7"; 5 | const SEQ_BITS = 12; 6 | const VAR = 0b10; 7 | const VAR_BITS = 2; 8 | const RAND_BITS = 62; 9 | 10 | function uuidv7Builder(getRandomValues: (array: Uint32Array) => Uint32Array) { 11 | let prevTimestamp = -1; 12 | let seq = 0; 13 | 14 | return () => { 15 | // Negative system clock adjustments are ignored to keep monotonicity 16 | const timestamp = Math.max(Date.now(), prevTimestamp); 17 | seq = timestamp === prevTimestamp ? seq + 1 : 0; 18 | prevTimestamp = timestamp; 19 | 20 | const var_rand = new Uint32Array(2); 21 | getRandomValues(var_rand); 22 | var_rand[0] = (VAR << (32 - VAR_BITS)) | (var_rand[0]! >>> VAR_BITS); 23 | 24 | return ( 25 | timestamp.toString(16).padStart(UNIX_TS_MS_BITS / 4, "0") + 26 | VER_DIGIT + 27 | seq.toString(16).padStart(SEQ_BITS / 4, "0") + 28 | var_rand[0]!.toString(16).padStart((VAR_BITS + RAND_BITS) / 2 / 4, "0") + 29 | var_rand[1]!.toString(16).padStart((VAR_BITS + RAND_BITS) / 2 / 4, "0") 30 | ); 31 | }; 32 | } 33 | 34 | export const uuidv7 = uuidv7Builder((array) => crypto.getRandomValues(array)); 35 | -------------------------------------------------------------------------------- /ts/packages/id/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [ 9 | { 10 | "path": "../util" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /ts/packages/instrument/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /ts/packages/instrument/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @aphro/instrument 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - publish for testing 8 | 9 | ## 0.0.6 10 | 11 | ### Patch Changes 12 | 13 | - Export queries and specs, move connectors to own packages, fix #43 and other bugs 14 | 15 | ## 0.0.5 16 | 17 | ### Patch Changes 18 | 19 | - transaction support 20 | 21 | ## 0.0.4 22 | 23 | ### Patch Changes 24 | 25 | - Strict mode for typescript, useEffect vs useSyncExternalStore, useLiveResult hook 26 | 27 | ## 0.0.3 28 | 29 | ### Patch Changes 30 | 31 | - rebuild -- last publish had a clobbered version of pnpm 32 | 33 | ## 0.0.2 34 | 35 | ### Patch Changes 36 | 37 | - workaround to adhere to strict mode in generated code #43 38 | -------------------------------------------------------------------------------- /ts/packages/instrument/README.md: -------------------------------------------------------------------------------- 1 | # instrument 2 | 3 | Helper functions to trace and measure program execution. 4 | -------------------------------------------------------------------------------- /ts/packages/instrument/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/instrument", 3 | "version": "0.1.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/instrument" 10 | }, 11 | "dependencies": { 12 | "@opentelemetry/api": "^1.1.0" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.18.13", 16 | "@babel/preset-env": "^7.18.10", 17 | "@types/jest": "^28.1.8", 18 | "jest": "^29.0.1", 19 | "typescript": "^4.8.2" 20 | }, 21 | "scripts": { 22 | "clean": "tsc --build --clean", 23 | "build": "tsc --build", 24 | "watch": "tsc --build -w", 25 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 26 | }, 27 | "jest": { 28 | "testMatch": [ 29 | "**/__tests__/**/*.test.js" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ts/packages/instrument/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as tracer, Tracer } from './tracer.js'; 2 | -------------------------------------------------------------------------------- /ts/packages/instrument/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [] 9 | } 10 | -------------------------------------------------------------------------------- /ts/packages/lazy-idb/notes.md: -------------------------------------------------------------------------------- 1 | One collection. 2 | Keeps everying by key-value. 3 | On start, load all into memory and re-wire references via ids. 4 | Persist ops received from persistor are normalized but still need a custom to-json to drop refs to models. 5 | 6 | on hydrate, must walk the entire object and children to re-write refs. 7 | refs encoded as: 8 | 9 | ```ts 10 | x: { 11 | __vulcan_ref: id; 12 | } 13 | ``` 14 | -------------------------------------------------------------------------------- /ts/packages/memory-db/notes.md: -------------------------------------------------------------------------------- 1 | - let people install listeners on the memory db so they can mark off what data is dirty and thus persist deltas. 2 | - deleted or created/updated 3 | - memory db needs a tx primitive... 4 | - we don't want to save the write of the thing to memory if the tx failed 5 | ^-- this is only the case if you're exposing a relational API over the mem db. 6 | not a problem if doc API. 7 | 8 | So MVP should be doc db and memory db doesn't actually exist except as a way to track events for persist plugins. 9 | -------------------------------------------------------------------------------- /ts/packages/migration/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /ts/packages/migration/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/packages/migration/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @aphro/migration-runtime-ts 2 | 3 | ## 0.2.0 4 | 5 | ### Minor Changes 6 | 7 | - publish for testing 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - @vlcn.io/config@0.3.0 13 | - @vlcn.io/sql@0.3.0 14 | - @vlcn.io/util@0.1.0 15 | 16 | ## 0.1.7 17 | 18 | ### Patch Changes 19 | 20 | - Export queries and specs, move connectors to own packages, fix #43 and other bugs 21 | - Updated dependencies 22 | - @aphro/context-runtime-ts@0.3.6 23 | - @aphro/sql-ts@0.2.6 24 | 25 | ## 0.1.6 26 | 27 | ### Patch Changes 28 | 29 | - transaction support 30 | - Updated dependencies 31 | - @aphro/context-runtime-ts@0.3.5 32 | - @aphro/sql-ts@0.2.5 33 | 34 | ## 0.1.5 35 | 36 | ### Patch Changes 37 | 38 | - Strict mode for typescript, useEffect vs useSyncExternalStore, useLiveResult hook 39 | - Updated dependencies 40 | - @aphro/context-runtime-ts@0.3.4 41 | - @aphro/sql-ts@0.2.4 42 | 43 | ## 0.1.4 44 | 45 | ### Patch Changes 46 | 47 | - rebuild -- last publish had a clobbered version of pnpm 48 | - Updated dependencies 49 | - @aphro/context-runtime-ts@0.3.3 50 | - @aphro/sql-ts@0.2.3 51 | 52 | ## 0.1.3 53 | 54 | ### Patch Changes 55 | 56 | - workaround to adhere to strict mode in generated code #43 57 | - Updated dependencies 58 | - @aphro/context-runtime-ts@0.3.2 59 | - @aphro/sql-ts@0.2.2 60 | 61 | ## 0.1.2 62 | 63 | ### Patch Changes 64 | 65 | - auto-create ids if not provided on create 66 | 67 | ## 0.1.1 68 | 69 | ### Patch Changes 70 | 71 | - generate bootstrapping utilities 72 | - Updated dependencies 73 | - @aphro/context-runtime-ts@0.3.1 74 | - @aphro/sql-ts@0.2.1 75 | 76 | ## 0.1.0 77 | 78 | ### Minor Changes 79 | 80 | - Simplify manual files, change output dir for generated code, allow caching in live queries, simplify 1 to 1 edge fetches 81 | 82 | ## 0.0.12 83 | 84 | ### Patch Changes 85 | 86 | - update dependency on strut/utils, enable manual methods for models 87 | 88 | ## 0.0.11 89 | 90 | ### Patch Changes 91 | 92 | - allow ephemeral nodes. allow type expressions for fields. 93 | 94 | ## 0.0.10 95 | 96 | ### Patch Changes 97 | 98 | - in-memory model support 99 | 100 | ## 0.0.9 101 | 102 | ### Patch Changes 103 | 104 | - update sid dependency 105 | -------------------------------------------------------------------------------- /ts/packages/migration/README.md: -------------------------------------------------------------------------------- 1 | # Migration Runtime 2 | 3 | Your DB is on the client's device and you pushed an udpated to your app that changes the DB schema! OMG! What DO!? 4 | 5 | The migration library provides a set of tools to enable you to manage and migrate database versions. 6 | 7 | ## Tools 8 | - Interact with db metadata (e.g., version, pull schemas) 9 | - Compare table schemas in app vs table schemas in db 10 | - Drop tables 11 | - Recreate tables 12 | - Calculate alter statements 13 | - Apply migrations 14 | 15 | Some prior art from the days when websql was a thing: 16 | - https://github.com/nanodeath/JS-Migrator/blob/master/migrator.js 17 | - https://blog.maxaller.name/2010/03/html5-web-sql-database-intro-to-versioning-and-migrations/ 18 | - https://gist.github.com/YannickGagnon/5320593 19 | 20 | # Ideas 21 | 22 | ## Yolo Mode 23 | 24 | 1. Drop mode 25 | 2. Auto-alter mode 26 | 27 | Drop mode just drops and re-creates the tables any time a delta is detected. 28 | 29 | Auto-alter diffs the (1) running schema with the (2) persisted schema. Deltas are converted to alter table statements and run. 30 | 31 | ## Versioned mode 32 | 33 | Versioned mode works by having a version in the schema file. 34 | 35 | Any time the version jumps the next codegen run will generate a migration script placeholder. This'll have generate alter statements within but also allow the author to insert their own logic. E.g., to map old columns to new. 36 | 37 | ## Logging 38 | 39 | We need to get data back about failed migrations from our clients. Is this opt-in? Do we create a package that can be included to gather error data and ship it back home? 40 | 41 | ## CRDTs 42 | 43 | We need to consider how we'll handle tables that are replicated p2p in the face of migrations. 44 | 1. We could stop clients from participating in the network until they've upgraded to the version of the most up to date client... 45 | 2. We could run migration scripts (the data transform not alter table parts) in-memory to move an old version write to a new one. This of course needs to be reversable too and... if fields are combined what happens to their clocks? 46 | 47 | Probably go with (1) for the MVP. (1) still has clock and versioning issues though... similar to what (2) had but we cut the bi-directional problem. 48 | 49 | 3. Counter 50 | 4. LWW registers 51 | 5. Sequence 52 | 6. clock push 53 | 7. Row level vs column level resolution 54 | 55 | ## Prior art 56 | 57 | https://github.com/groue/GRDB.swift/blob/master/README.md (see their migrations syntax and handling -- pretty nice) -------------------------------------------------------------------------------- /ts/packages/migration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/migration", 3 | "version": "0.2.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/migration" 10 | }, 11 | "dependencies": { 12 | "@vlcn.io/config": "workspace:*", 13 | "@vlcn.io/sql": "workspace:*", 14 | "@databases/sqlite": "^4.0.1", 15 | "@vlcn.io/util": "workspace:*" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.18.13", 19 | "@babel/preset-env": "^7.18.10", 20 | "@types/jest": "^28.1.8", 21 | "@types/prettier": "^2.7.0", 22 | "@typescript-eslint/typescript-estree": "^5.35.1", 23 | "jest": "^29.0.1", 24 | "md5": "^2.3.0", 25 | "prettier": "^2.7.1", 26 | "sql-formatter": "^10.0.0", 27 | "typescript": "^4.8.2" 28 | }, 29 | "scripts": { 30 | "clean": "tsc --build --clean", 31 | "build": "tsc --build", 32 | "watch": "tsc --build -w", 33 | "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js", 34 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 35 | }, 36 | "jest": { 37 | "testMatch": [ 38 | "**/__tests__/**/*.test.js" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ts/packages/migration/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bootstrap.js'; 2 | 3 | // export function getTableSchemasFromStorage(db: SQLResolvedDB): Map { 4 | // return new Map(); 5 | // } 6 | 7 | // export function getDbVersion(db: SQLResolvedDB): string { 8 | // return ''; 9 | // } 10 | 11 | // export function getSchemaDeltas(leftSql: string, rightSql: string) {} 12 | 13 | /* 14 | Should we instead persist the aphro schema version into the metadata table? 15 | And operations would be against aphro schemas... 16 | We'd have to ship the parser and sql codegen in order to enable this. 17 | 18 | What does this workflow look like? 19 | You'd snapshot your schema at a given version? 20 | 21 | Should we start with a `yolo` mode that auto runs alter tables as it finds deltas? 22 | 23 | A non-yolo mode would user versioning and any time there's a bump between versions the author 24 | must provide a migration script. 25 | 26 | We can generate a starter-script for them that has the alter table commands. 27 | 28 | Bootstrapping should 29 | 0. open tx 30 | 1. Create tables 31 | 2. Create / update metadata table 32 | 3. Persist current aphrodite schema versions in metadata table 33 | 34 | We need to ship with both a: 35 | 1. Aphrodite schema map 36 | 2. SQL schema map 37 | 38 | Or we can ship with just the former and gen the latter as needed... 39 | 40 | 41 | */ 42 | 43 | // Bootstrap function that takes in `domain.aphro` and a connection. 44 | // Will thus need to depend on sql codegen package. 45 | -------------------------------------------------------------------------------- /ts/packages/migration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [ 9 | { 10 | "path": "../sql" 11 | }, 12 | { 13 | "path": "../config" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /ts/packages/model-persisted/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @vlcn.io/model-persisted 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - publish for testing 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - @vlcn.io/config@0.3.0 13 | - @vlcn.io/id@0.1.0 14 | - @vlcn.io/model@0.1.0 15 | - @vlcn.io/schema-api@0.3.0 16 | - @vlcn.io/util@0.1.0 17 | - @vlcn.io/value@0.1.0 18 | - @vlcn.io/zone@0.1.0 19 | 20 | ## 0.0.3 21 | 22 | ### Patch Changes 23 | 24 | - retryable transactions, serializable transactions, conflict detection 25 | - Updated dependencies 26 | - @vlcn.io/id@0.0.2 27 | - @vlcn.io/model@0.0.3 28 | - @vlcn.io/util@0.0.2 29 | - @vlcn.io/value@0.0.3 30 | 31 | ## 0.0.2 32 | 33 | ### Patch Changes 34 | 35 | - Updated dependencies 36 | - @vlcn.io/value@0.0.2 37 | - @vlcn.io/model@0.0.2 38 | -------------------------------------------------------------------------------- /ts/packages/model-persisted/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /ts/packages/model-persisted/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/model-persisted", 3 | "version": "0.1.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/model-persisted" 10 | }, 11 | "dependencies": { 12 | "@vlcn.io/config": "workspace:*", 13 | "@vlcn.io/model": "workspace:*", 14 | "@vlcn.io/id": "workspace:*", 15 | "@vlcn.io/util": "workspace:*", 16 | "@vlcn.io/value": "workspace:*", 17 | "@vlcn.io/schema-api": "workspace:*", 18 | "@vlcn.io/zone": "workspace:*" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.18.13", 22 | "@babel/preset-env": "^7.18.10", 23 | "@types/jest": "^28.1.8", 24 | "fast-check": "^3.1.2", 25 | "jest": "^29.0.1", 26 | "typescript": "^4.8.2" 27 | }, 28 | "scripts": { 29 | "clean": "tsc --build --clean", 30 | "build": "tsc --build", 31 | "watch": "tsc --build -w", 32 | "test": "node ./node_modules/jest/bin/jest.js", 33 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 34 | }, 35 | "jest": { 36 | "testMatch": [ 37 | "**/__tests__/**/*.test.js" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ts/packages/model-persisted/src/AsyncPersistedModel.ts: -------------------------------------------------------------------------------- 1 | import { config } from "@vlcn.io/config"; 2 | import { ID_of } from "@vlcn.io/id"; 3 | import { 4 | IPersistedModel, 5 | IPersistedModelCtor, 6 | PersistedModel, 7 | pullId, 8 | } from "./PersistedModel.js"; 9 | import { asyncPersistor } from "./persistor.js"; 10 | import { BasePersistedModelData } from "./spec.js"; 11 | 12 | export abstract class AsyncPersistedModel< 13 | T extends BasePersistedModelData 14 | > extends PersistedModel { 15 | static async createOrUpdate< 16 | D extends BasePersistedModelData, 17 | M extends IPersistedModel 18 | >(ctor: IPersistedModelCtor, data: D | Omit): Promise { 19 | const id = pullId(ctor, data) as ID_of; 20 | 21 | const existing = config.cache.get( 22 | id, 23 | ctor.spec.storage.db, 24 | ctor.spec.storage.tablish 25 | ); 26 | 27 | // we add to the cache regardless of transaction outcome -- 28 | // so the thing could be in there from a failed tx. 29 | // update it to final state if so. 30 | if (existing) { 31 | await existing.update(data as D); 32 | return existing; 33 | } 34 | 35 | const model = new ctor(data, "create"); 36 | config.cache.set( 37 | id, 38 | model, 39 | ctor.spec.storage.db, 40 | ctor.spec.storage.tablish 41 | ); 42 | 43 | await asyncPersistor.create(model); 44 | return model; 45 | } 46 | 47 | // TODO: create, update, delete 48 | // need to assert that we're in the correct transaction type. 49 | // If we're not in a durable tx then we should upgrade it to durable. 50 | // and have the durable commit occur at the end 51 | // TODO: codegen `update`, `delete`, `create` 52 | // to be sync or async depending on backing storage. 53 | // in-memory models will be synchronous. 54 | async update(updates: Partial): Promise { 55 | if (this.isNoop(updates)) { 56 | return; 57 | } 58 | 59 | this.value.val = Object.freeze({ 60 | ...this.value.val, 61 | ...updates, 62 | }); 63 | 64 | return await asyncPersistor.update(this); 65 | } 66 | 67 | delete(): Promise { 68 | // GC will evict from the cache as needed. 69 | // cache.remove(this); 70 | return asyncPersistor.delete(this); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ts/packages/model-persisted/src/SyncPersistedModel.ts: -------------------------------------------------------------------------------- 1 | import { config } from "@vlcn.io/config"; 2 | import { ID_of } from "@vlcn.io/id"; 3 | import { 4 | IPersistedModel, 5 | IPersistedModelCtor, 6 | PersistedModel, 7 | pullId, 8 | } from "./PersistedModel.js"; 9 | import { syncPersistor } from "./persistor.js"; 10 | import { BasePersistedModelData } from "./spec.js"; 11 | 12 | /** 13 | * We have two types here -- sync and async -- depending on the storage backend 14 | * used. 15 | * 16 | * If the model is served by in-memory storage than all methods (create, update, delete) and queries to it 17 | * can be synchronous. 18 | * 19 | * Synchronous models may still have edges to asynchronous models. Those edge queries 20 | * will be async as their "color" is determined by the thing being fetched. 21 | */ 22 | export abstract class SyncPersistedModel< 23 | T extends BasePersistedModelData 24 | > extends PersistedModel { 25 | static createOrUpdate< 26 | D extends BasePersistedModelData, 27 | M extends IPersistedModel 28 | >(ctor: IPersistedModelCtor, data: D | Omit): M { 29 | const id = pullId(ctor, data) as ID_of; 30 | 31 | const existing = config.cache.get( 32 | id, 33 | ctor.spec.storage.db, 34 | ctor.spec.storage.tablish 35 | ); 36 | 37 | // we add to the cache regardless of transaction outcome -- 38 | // so the thing could be in there from a failed tx. 39 | // update it to final state if so. 40 | if (existing) { 41 | existing.update(data as D); 42 | return existing; 43 | } 44 | 45 | // TODO: rename `create` to `upsert` to update rather than create 46 | // on conflict. In a replicated world we could issue a create 47 | // that should be an update. 48 | const model = new ctor(data, "create"); 49 | config.cache.set( 50 | id, 51 | model, 52 | ctor.spec.storage.db, 53 | ctor.spec.storage.tablish 54 | ); 55 | syncPersistor.create(model); 56 | return model; 57 | } 58 | 59 | update(updates: Partial): void { 60 | if (this.isNoop(updates)) { 61 | return; 62 | } 63 | 64 | this.value.val = Object.freeze({ 65 | ...this.value.val, 66 | ...updates, 67 | }); 68 | 69 | syncPersistor.update(this); 70 | } 71 | 72 | delete(): void { 73 | // GC will evict from the cache as needed. 74 | // cache.remove(this); 75 | return syncPersistor.delete(this); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ts/packages/model-persisted/src/datasetKey.ts: -------------------------------------------------------------------------------- 1 | import { JunctionEdgeSpec, NodeSpec } from "@vlcn.io/schema-api"; 2 | 3 | export type DatasetKey = string; 4 | 5 | export default function specToDatasetKey( 6 | spec: NodeSpec | JunctionEdgeSpec 7 | ): DatasetKey { 8 | return ( 9 | spec.storage.engine + "-" + spec.storage.db + "-" + spec.storage.tablish 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /ts/packages/model-persisted/src/index.ts: -------------------------------------------------------------------------------- 1 | export { IPersistedModel, PersistedModel } from "./PersistedModel.js"; 2 | export { default as specToDatasetKey } from "./datasetKey.js"; 3 | export * from "./spec.js"; 4 | export { SyncPersistedModel } from "./SyncPersistedModel.js"; 5 | export { AsyncPersistedModel } from "./AsyncPersistedModel.js"; 6 | export { default as modelGenMemo } from "./modelGenMemo.js"; 7 | -------------------------------------------------------------------------------- /ts/packages/model-persisted/src/modelGenMemo.ts: -------------------------------------------------------------------------------- 1 | import { config } from "@vlcn.io/config"; 2 | import { ID_of } from "@vlcn.io/id"; 3 | 4 | /** 5 | * Memoizes `gen`, `genFoo` type methods where we're loading a model by id. 6 | * 7 | * If multiple concurrent access to the method happens, return the promise awaiting to be resolved. 8 | * If the thing is already cached, returns that. 9 | * 10 | * TODO: can we move this deeper into the query layer itself? 11 | * TODO: apply this to 1-1 edges too. E.g., `deck->genOwner` 12 | */ 13 | export default function modelGenMemo( 14 | dbname: string, 15 | tablish: string, 16 | gen: (id: ID_of) => Promise 17 | ) { 18 | const priorHandles: Map> = new Map(); 19 | return async (id: ID_of) => { 20 | const priorHandle = priorHandles.get(id); 21 | if (priorHandle) { 22 | return priorHandle; 23 | } 24 | const existing = config.cache.get(id as ID_of, dbname, tablish); 25 | if (existing != null) { 26 | return existing; 27 | } 28 | const currentHandle = gen(id); 29 | priorHandles.set(id, currentHandle); 30 | currentHandle.finally(() => priorHandles.delete(id)); 31 | return currentHandle; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /ts/packages/model-persisted/src/spec.ts: -------------------------------------------------------------------------------- 1 | import { NodeSpec, JunctionEdgeSpec } from "@vlcn.io/schema-api"; 2 | import { ID_of } from "@vlcn.io/id"; 3 | import { IPersistedModel } from "./PersistedModel"; 4 | 5 | // TODO: update this to not require id. edges don't have a single id field for example. 6 | // and users can define their own primary key field names 7 | export type BasePersistedModelData = { id: ID_of }; 8 | 9 | export type ModelCreate< 10 | M extends IPersistedModel, 11 | D extends BasePersistedModelData 12 | > = { 13 | // TODO: differentiate between async and sync model create 14 | create(data: D /*raw?: boolean*/): Promise; 15 | hydrate(data: D): M; 16 | }; 17 | 18 | export interface INode 19 | extends IPersistedModel { 20 | readonly id: ID_of; 21 | readonly spec: NodeSpecWithCreate; 22 | } 23 | 24 | export interface IEdge 25 | extends IPersistedModel { 26 | readonly spec: EdgeSpecWithCreate; 27 | } 28 | 29 | export type NodeSpecWithCreate< 30 | M extends INode, 31 | D extends BasePersistedModelData 32 | > = ModelCreate & NodeSpec; 33 | 34 | export type EdgeSpecWithCreate< 35 | M extends IEdge, 36 | D extends BasePersistedModelData 37 | > = ModelCreate & JunctionEdgeSpec; 38 | 39 | export type ModelSpecWithCreate< 40 | M extends IPersistedModel, 41 | D extends BasePersistedModelData 42 | > = ModelCreate & (NodeSpec | JunctionEdgeSpec); 43 | -------------------------------------------------------------------------------- /ts/packages/model-persisted/src/syncAsyncCommon.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlcn-io/model/7d3850fc5d1f0b2ed52d721edb898fccc21cb90f/ts/packages/model-persisted/src/syncAsyncCommon.ts -------------------------------------------------------------------------------- /ts/packages/model-persisted/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [ 9 | { "path": "../model" }, 10 | { "path": "../id" }, 11 | { "path": "../util" }, 12 | { "path": "../value" }, 13 | { "path": "../config" } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /ts/packages/model-pfr/MemoryDB.ts: -------------------------------------------------------------------------------- 1 | // import { Key, Row } from "./Row.js"; 2 | 3 | // export type MemoryReadQuery = { 4 | // type: "read"; 5 | // tableName: string; 6 | // // undefined --> all 7 | // // [] --> none 8 | // // [id, ...] --> specific ids 9 | // roots?: Key[]; 10 | // }; 11 | 12 | // export type MemoryWriteQuery = { 13 | // type: "write"; 14 | // op: "delete" | "upsert"; 15 | // tableName: string; 16 | // rows: Row[]; 17 | // }; 18 | 19 | // export type MemoryQuery = MemoryReadQuery | MemoryWriteQuery; 20 | 21 | // /** 22 | // * Holds all in-memory nodes in-memory. 23 | // */ 24 | // export default class MemoryDB { 25 | // private tables: Map = new Map(); 26 | 27 | // async read(q: MemoryReadQuery): Promise { 28 | // const table = this.tables.get(q.tableName); 29 | // if (table == null) { 30 | // return []; 31 | // } 32 | 33 | // if (q.roots == null) { 34 | // return Object.values(table); 35 | // } 36 | 37 | // return q.roots.map((r) => table[r]); 38 | // } 39 | 40 | // async write(q: MemoryWriteQuery): Promise { 41 | // const c = this.tables.get(q.tableName); 42 | // let table: { [key: Key]: any }; 43 | // // To make the type checker happy 44 | // if (c == null) { 45 | // table = {}; 46 | // this.tables.set(q.tableName, table); 47 | // } else { 48 | // table = c; 49 | // } 50 | 51 | // switch (q.op) { 52 | // case "delete": 53 | // q.rows.forEach((m) => delete table[m.id]); 54 | // case "upsert": 55 | // q.rows.forEach((m) => (table[m.id] = m)); 56 | // } 57 | // } 58 | 59 | // dispose(): void { 60 | // this.tables = new Map(); 61 | // } 62 | // } 63 | -------------------------------------------------------------------------------- /ts/packages/model-pfr/README.md: -------------------------------------------------------------------------------- 1 | A concept to explore in the future: Post-Facto Relational. 2 | 3 | The user creates their data as normal types within their programming language. 4 | 5 | The user also defines "roots" that should never be deleted. E.g., a user type. 6 | 7 | We augment instances types with a uuid on create. 8 | 9 | For each new type (class) we create a relation (table) that has weak refs to all instances of that type. 10 | 11 | Whenever an instance is updated or created, we crawl it for pointers to other model instances. We then index and model these appropriately as foreign key or junction relationships. 12 | 13 | Persistence persists the relations and replaces pointers with uuid refs. 14 | 15 | Re-hydration pulls from relations and follows pointers if/when needed. 16 | 17 | I.e., We create a relational model by normalizing the denormalized representation. We can then give the user query APIs to interface with their data in a relational manner when needed. 18 | -------------------------------------------------------------------------------- /ts/packages/model-pfr/Table.ts: -------------------------------------------------------------------------------- 1 | export default class Table {} 2 | 3 | /** 4 | * Tables need an operation based implementation? 5 | * for creates and deletes. 6 | * 7 | * On create of a relational model we need to: 8 | * 1. tell the relation we created a thing 9 | * 2. store the created thing in tx buffer 10 | * 11 | * When reading/querying we need to: 12 | * 1. query the table 13 | * 2. query the table in the tx buffer if it exists 14 | * 3. concat the results 15 | * 4. apply further filters after 16 | * 17 | * I.e., base table + tx table (WAL) are unioned. 18 | * 19 | * Well... if the WAL has _deletes_ we need to retract those from the base table. 20 | * 21 | * So it isn't quite a union. 22 | * 23 | * We need a "unified" interface that combines TX memory + Base memory. 24 | * 25 | * TX memory holds [delete, id][] and/or [create, model][] operations for a given collection. 26 | * 27 | * A commit of the tx writes these ops to the base tables. 28 | */ 29 | -------------------------------------------------------------------------------- /ts/packages/model/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @vlcn.io/model 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - publish for testing 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - @vlcn.io/value@0.1.0 13 | 14 | ## 0.0.3 15 | 16 | ### Patch Changes 17 | 18 | - retryable transactions, serializable transactions, conflict detection 19 | - Updated dependencies 20 | - @vlcn.io/value@0.0.3 21 | 22 | ## 0.0.2 23 | 24 | ### Patch Changes 25 | 26 | - Updated dependencies 27 | - @vlcn.io/value@0.0.2 28 | -------------------------------------------------------------------------------- /ts/packages/model/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /ts/packages/model/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/model", 3 | "version": "0.1.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/model" 10 | }, 11 | "dependencies": { 12 | "@vlcn.io/value": "workspace:*", 13 | "typescript": "^4.8.2" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": "^7.18.13", 17 | "@babel/preset-env": "^7.18.10", 18 | "@types/jest": "^28.1.8", 19 | "fast-check": "^3.1.2", 20 | "jest": "^29.0.1" 21 | }, 22 | "scripts": { 23 | "clean": "tsc --build --clean", 24 | "build": "tsc --build", 25 | "watch": "tsc --build -w", 26 | "test": "node ./node_modules/jest/bin/jest.js", 27 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 28 | }, 29 | "jest": { 30 | "testMatch": [ 31 | "**/__tests__/**/*.test.js" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ts/packages/model/src/index.ts: -------------------------------------------------------------------------------- 1 | export { IModel, Model } from "./Model.js"; 2 | -------------------------------------------------------------------------------- /ts/packages/model/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [{ "path": "../value" }] 9 | } 10 | -------------------------------------------------------------------------------- /ts/packages/query/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | -------------------------------------------------------------------------------- /ts/packages/query/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/packages/query/README.md: -------------------------------------------------------------------------------- 1 | # Query 2 | 3 | The query layer built atop schemas -------------------------------------------------------------------------------- /ts/packages/query/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | targets: { 7 | node: "current", 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /ts/packages/query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/query", 3 | "version": "0.4.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/query" 10 | }, 11 | "dependencies": { 12 | "@vlcn.io/cache": "workspace:*", 13 | "@vlcn.io/config": "workspace:*", 14 | "@vlcn.io/instrument": "workspace:*", 15 | "@vlcn.io/model-persisted": "workspace:*", 16 | "@vlcn.io/schema-api": "workspace:*", 17 | "@vlcn.io/sql": "workspace:*", 18 | "@vlcn.io/id": "workspace:*", 19 | "@vlcn.io/util": "workspace:*" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.18.13", 23 | "@babel/preset-env": "^7.18.10", 24 | "@types/jest": "^28.1.8", 25 | "@typescript-eslint/typescript-estree": "^5.35.1", 26 | "fast-check": "^3.1.2", 27 | "jest": "^29.0.1", 28 | "typescript": "^4.8.2" 29 | }, 30 | "scripts": { 31 | "clean": "tsc --build --clean", 32 | "build": "tsc --build", 33 | "watch": "tsc --build -w", 34 | "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js", 35 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 36 | }, 37 | "jest": { 38 | "testMatch": [ 39 | "**/__tests__/**/*.test.js" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ts/packages/query/src/CountLoadExpression.ts: -------------------------------------------------------------------------------- 1 | import { ChunkIterable } from "./ChunkIterable"; 2 | import { DerivedExpression } from "./Expression"; 3 | 4 | /** 5 | * More context on Expressions exists in `./Expression.ts` 6 | * 7 | * A ModelLoadExpression is a DerivedExpression that converts raw data 8 | * returned by the data source into `Aphrodite` model instances. 9 | * 10 | * E.g., If you defined a `Todo` node in your schema then queried all todos, 11 | * a `ModelLoadExpression` is added to this query to convert the rows to `Todo` instances. 12 | */ 13 | export default class CountLoadExpression 14 | implements DerivedExpression 15 | { 16 | readonly type = "countLoad"; 17 | constructor() {} 18 | 19 | chainAfter(iterable: ChunkIterable) { 20 | return iterable.map((d) => (d as any)["count(*)"]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ts/packages/query/src/ExpressionVisitor.ts: -------------------------------------------------------------------------------- 1 | import { filter, hop, orderBy, take } from "./Expression.js"; 2 | 3 | export interface ExpressionVisitor { 4 | filter(f: ReturnType): TRet; 5 | orderBy(o: ReturnType): TRet; 6 | limit(l: ReturnType): TRet; 7 | hop(h: ReturnType): TRet; 8 | } 9 | -------------------------------------------------------------------------------- /ts/packages/query/src/Field.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BasePersistedModelData, 3 | IPersistedModel, 4 | } from "@vlcn.io/model-persisted"; 5 | 6 | export interface FieldGetter { 7 | readonly get: (m: Tm) => Tv; 8 | } 9 | 10 | /** 11 | * Don't be confused by `ModelFieldGetter` 12 | * You may see `model._get(fieldName)` and assume that we're not doing filters 13 | * in the database but instead loading the model into the application before filtering it. 14 | * 15 | * This is not the case. ModelFieldGetters are optimized away into SQL statements whenever possible. 16 | * They have logic to fetch data from the model in the rare case they can not be optimized (e.g., the user 17 | * filters based on arbitrary logic and not based on an expression supported in SQL). 18 | * 19 | * Read more about query optimization here: https://tantaman.com/2022-05-26-query-plan-optimization.html 20 | */ 21 | export class ModelFieldGetter< 22 | Tk extends keyof Td, 23 | Td extends BasePersistedModelData, 24 | Tm extends IPersistedModel 25 | > implements FieldGetter 26 | { 27 | constructor(public readonly fieldName: Tk) {} 28 | 29 | get(model: Tm): Td[Tk] { 30 | return model.data[this.fieldName]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ts/packages/query/src/HopPlan.ts: -------------------------------------------------------------------------------- 1 | import { ChunkIterable } from './ChunkIterable.js'; 2 | import { Expression, HopExpression } from './Expression.js'; 3 | import { IPlan } from './Plan.js'; 4 | 5 | /** 6 | * Hop plans hold all expressions that should be executed _after_ traversing a given 7 | * hop (edge) but before traversing the next hop (edge). 8 | * 9 | * E.g., A --> B --> C 10 | * 11 | * A HopPlan exists from A --> B and another from B --> C. 12 | * 13 | * The first hop plan encodes any derived expressions that occur against the data 14 | * loaded by the A --> B hop. 15 | * 16 | * The second hop plan encodes any derived expressions that occur against the data 17 | * loaded by the B --> C hop. 18 | * 19 | * See more on query planning here: 20 | * https://tantaman.com/2022-05-26-query-planning 21 | */ 22 | export default class HopPlan implements IPlan { 23 | constructor( 24 | public readonly sourcePlan: IPlan, 25 | public readonly hop: HopExpression, 26 | private derivs: Expression[], 27 | ) {} 28 | 29 | get derivations(): ReadonlyArray { 30 | return this.derivs; 31 | } 32 | 33 | get iterable(): ChunkIterable { 34 | const iterable = this.hop.chainAfter(this.sourcePlan.iterable); 35 | return this.derivs.reduce((iterable, expression) => expression.chainAfter(iterable), iterable); 36 | } 37 | 38 | async gen(): Promise { 39 | let results: any[] = []; 40 | for await (const chunk of this.iterable) { 41 | results = results.concat(chunk); 42 | } 43 | 44 | return results; 45 | } 46 | 47 | addDerivation(expression?: Expression): this { 48 | if (!expression) { 49 | return this; 50 | } 51 | 52 | this.derivs.push(expression); 53 | 54 | return this; 55 | } 56 | 57 | /** 58 | * Queries are built up into a reverse linked list. 59 | * The last query is what the user executes. 60 | * This last query will optimize from the end back on down. 61 | */ 62 | optimize(nextHop?: HopPlan): IPlan { 63 | // Optimize our hop and fold in the next hop 64 | const optimizedPlanForThisHop = this.hop.optimize(this.sourcePlan, this, nextHop); 65 | return this.sourcePlan.optimize(optimizedPlanForThisHop); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ts/packages/query/src/ModelLoadExpression.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BasePersistedModelData, 3 | IPersistedModel, 4 | } from "@vlcn.io/model-persisted"; 5 | import { ChunkIterable, SyncMappedChunkIterable } from "./ChunkIterable"; 6 | import { DerivedExpression } from "./Expression"; 7 | 8 | /** 9 | * More context on Expressions exists in `./Expression.ts` 10 | * 11 | * A ModelLoadExpression is a DerivedExpression that converts raw data 12 | * returned by the data source into `Aphrodite` model instances. 13 | * 14 | * E.g., If you defined a `Todo` node in your schema then queried all todos, 15 | * a `ModelLoadExpression` is added to this query to convert the rows to `Todo` instances. 16 | */ 17 | export default class ModelLoadExpression< 18 | TData extends BasePersistedModelData, 19 | TModel extends IPersistedModel 20 | > implements DerivedExpression 21 | { 22 | readonly type = "modelLoad"; 23 | constructor(private factory: (data: TData) => TModel) {} 24 | 25 | chainAfter(iterable: ChunkIterable) { 26 | // TODO: this factory better had call hydrate. 27 | throw new Error("Are you calling hydrate?"); 28 | return iterable.map((d) => this.factory(d)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ts/packages/query/src/Plan.ts: -------------------------------------------------------------------------------- 1 | import { ChunkIterable } from './ChunkIterable.js'; 2 | import { Expression, SourceExpression } from './Expression.js'; 3 | import HopPlan from './HopPlan.js'; 4 | 5 | /** 6 | * Interface for a query plan! What is a query plan? Why 7 | * does an ORM need a query plan given its just going to generate 8 | * SQL at the end? 9 | * 10 | * Good questions. See https://tantaman.com/2022-05-26-query-planning 11 | */ 12 | export interface IPlan { 13 | get derivations(): readonly Expression[]; 14 | get iterable(): ChunkIterable; 15 | addDerivation(expression?: Expression): this; 16 | optimize(nextHop?: HopPlan): IPlan; 17 | gen(): Promise; 18 | } 19 | 20 | export default class Plan implements IPlan { 21 | constructor( 22 | public readonly source: SourceExpression, 23 | public readonly derivations: Expression[], 24 | ) {} 25 | 26 | get iterable(): ChunkIterable /* final TOut */ { 27 | const iterable = this.source.iterable; 28 | return this.derivations.reduce( 29 | (iterable, expression) => expression.chainAfter(iterable), 30 | iterable, 31 | ); 32 | } 33 | 34 | async gen(): Promise /* final TOut[] */ { 35 | let results: any[] = []; 36 | for await (const chunk of this.iterable) { 37 | results = results.concat(chunk); 38 | } 39 | 40 | return results; 41 | } 42 | 43 | addDerivation(expression?: Expression): this { 44 | if (!expression) { 45 | return this; 46 | } 47 | 48 | this.derivations.push(expression); 49 | 50 | return this; 51 | } 52 | 53 | optimize(nextHop?: HopPlan) { 54 | return this.source.optimize(this, nextHop); 55 | } 56 | 57 | // partition(): [Plan, ...HopPlan] { 58 | // const sourcePlan = 59 | // } 60 | } 61 | -------------------------------------------------------------------------------- /ts/packages/query/src/QueryFactory.ts: -------------------------------------------------------------------------------- 1 | import { EdgeSpec } from "@vlcn.io/schema-api"; 2 | import { assertUnreachable } from "@vlcn.io/util"; 3 | import MemorySourceQuery from "./memory/MemorySourceQuery.js"; 4 | import MemoryHopQuery from "./memory/MemoryHopQuery.js"; 5 | import { DerivedQuery, HopQuery, Query } from "./Query.js"; 6 | import SQLHopQuery from "./sql/SQLHopQuery.js"; 7 | import SQLSourceQuery from "./sql/SQLSourceQuery.js"; 8 | import { 9 | BasePersistedModelData, 10 | IPersistedModel, 11 | ModelSpecWithCreate, 12 | } from "@vlcn.io/model-persisted"; 13 | 14 | // Runtime factory so we can swap to `Wire` when running on a client vs 15 | // the native platform. 16 | const factory = { 17 | createSourceQueryFor>( 18 | spec: ModelSpecWithCreate 19 | ): Query { 20 | switch (spec.storage.type) { 21 | case "sql": 22 | return new SQLSourceQuery(spec); 23 | case "memory": 24 | return new MemorySourceQuery(spec); 25 | default: 26 | throw new Error(spec.storage.type + " is not yet supported"); 27 | } 28 | }, 29 | 30 | // TODO: get types into the edge specs so our hop and have types? 31 | createHopQueryFor( 32 | priorQuery: DerivedQuery, 33 | edge: EdgeSpec 34 | ): HopQuery { 35 | const type = edge.dest.storage.type; 36 | switch (type) { 37 | case "sql": 38 | return SQLHopQuery.create(priorQuery, edge); 39 | case "memory": 40 | return MemoryHopQuery.create(priorQuery, edge); 41 | case "ephemeral": 42 | throw new Error(`Hops for ${type} are not implemented yet`); 43 | } 44 | assertUnreachable(type); 45 | }, 46 | }; 47 | 48 | export default factory; 49 | -------------------------------------------------------------------------------- /ts/packages/query/src/StorageAdapter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Storage adapter should exist elsewhere? 3 | At a higher level where we have dependencies for things such as dexie or sql or cypher. 4 | */ 5 | -------------------------------------------------------------------------------- /ts/packages/query/src/explain/__tests__/orderPlans.test.ts: -------------------------------------------------------------------------------- 1 | import { ChunkIterable, emptyChunkIterable } from '../../ChunkIterable.js'; 2 | import { Expression, HopExpression } from '../../Expression.js'; 3 | import HopPlan from '../../HopPlan.js'; 4 | import { IPlan } from '../../Plan.js'; 5 | import orderPlans from '../orderPlans.js'; 6 | 7 | class TestSourcePlan implements IPlan { 8 | get derivations(): readonly Expression[] { 9 | return []; 10 | } 11 | 12 | addDerivation(expression?: Expression): this { 13 | return this; 14 | } 15 | 16 | get iterable(): ChunkIterable { 17 | return emptyChunkIterable; 18 | } 19 | 20 | async gen(): Promise { 21 | return []; 22 | } 23 | 24 | optimize(nextHop?: HopPlan): IPlan { 25 | return this; 26 | } 27 | } 28 | 29 | class TestHopExpression implements HopExpression { 30 | chainAfter(iterable: ChunkIterable): ChunkIterable { 31 | return emptyChunkIterable; 32 | } 33 | optimize(sourcePlan: IPlan, plan: HopPlan, nextHop?: HopPlan): HopPlan { 34 | return plan; 35 | } 36 | type: 'hop' = 'hop'; 37 | implicatedDataset(): string { 38 | return '---'; 39 | } 40 | } 41 | 42 | test('re-orders hop plans', () => { 43 | const source = new TestSourcePlan(); 44 | const hop1 = new HopPlan(source, new TestHopExpression(), []); 45 | const hop2 = new HopPlan(hop1, new TestHopExpression(), []); 46 | 47 | expect(orderPlans(hop2)).toEqual([source, hop1, hop2]); 48 | }); 49 | -------------------------------------------------------------------------------- /ts/packages/query/src/explain/orderPlans.ts: -------------------------------------------------------------------------------- 1 | import HopPlan from '../HopPlan.js'; 2 | import { IPlan } from '../Plan.js'; 3 | 4 | export default function orderPlans(plan: IPlan): IPlan[] { 5 | const plans: IPlan[] = []; 6 | let sourcePlan: IPlan | null = plan; 7 | while (sourcePlan != null) { 8 | plans.push(sourcePlan); 9 | if (sourcePlan instanceof HopPlan) { 10 | sourcePlan = sourcePlan.sourcePlan; 11 | } else { 12 | sourcePlan = null; 13 | } 14 | } 15 | 16 | plans.reverse(); 17 | return plans; 18 | } 19 | -------------------------------------------------------------------------------- /ts/packages/query/src/explain/printPlan.ts: -------------------------------------------------------------------------------- 1 | import { IPlan } from '../Plan.js'; 2 | import orderPlans from './orderPlans.js'; 3 | 4 | export default function printPlan(plan: IPlan) { 5 | // Query plans can be chained after other query plans. 6 | // To get the in-order list of plans, we need to go backwards through the linked list of plans, 7 | // push each onto an array and then reverse the array. 8 | const plans = orderPlans(plan); 9 | 10 | // TODO: make this more sophisticated and visit expressions and so on. 11 | for (const p of plans) { 12 | console.log(p); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ts/packages/query/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as P, Predicate } from './Predicate.js'; 2 | export { Query, DerivedQuery, UpdateType, EmptyQuery } from './Query.js'; 3 | export { default as QueryFactory } from './QueryFactory.js'; 4 | export * from './Expression.js'; 5 | export * from './Field.js'; 6 | export { default as printPlan } from './explain/printPlan.js'; 7 | export { default as LiveResult } from './live/LiveResult.js'; 8 | -------------------------------------------------------------------------------- /ts/packages/query/src/live/__tests__/LiveResult.test.ts: -------------------------------------------------------------------------------- 1 | test('Generators', () => { 2 | // This is tested in `integration-tests-ts` 3 | }); 4 | -------------------------------------------------------------------------------- /ts/packages/query/src/memory/MemoryHopChunkIterable.ts: -------------------------------------------------------------------------------- 1 | import { MemoryResolvedDB, config } from "@vlcn.io/config"; 2 | import { IPersistedModel } from "@vlcn.io/model-persisted"; 3 | import { EdgeSpec } from "@vlcn.io/schema-api"; 4 | import { BaseChunkIterable, ChunkIterable } from "../ChunkIterable.js"; 5 | import { HoistedOperations } from "./MemorySourceExpression.js"; 6 | 7 | export default class MemoryHopChunkIterable< 8 | TIn extends IPersistedModel, 9 | TOut 10 | > extends BaseChunkIterable { 11 | constructor( 12 | private readonly edge: EdgeSpec, 13 | private readonly ops: HoistedOperations, 14 | private readonly source: ChunkIterable 15 | ) { 16 | super(); 17 | } 18 | 19 | async *[Symbol.asyncIterator](): AsyncIterator { 20 | const db = config.storage( 21 | this.edge.dest.storage.engine, 22 | this.edge.dest.storage.db 23 | ) as MemoryResolvedDB; 24 | for await (const chunk of this.source) { 25 | // field edge -> roots 26 | // fk edge -> no roots, filter 27 | // jx edge -> root on jx table followed by roots on final dest 28 | switch (this.edge.type) { 29 | case "field": 30 | yield await db.read({ 31 | type: "read", 32 | tablish: this.edge.dest.storage.tablish, 33 | // @ts-ignore 34 | roots: chunk.map((c) => c.data[this.edge.sourceField]), 35 | }); 36 | break; 37 | case "foreignKey": 38 | // TODO: memoize this given you're re-reading all on every chunk 39 | const all = await db.read({ 40 | type: "read", 41 | tablish: this.edge.dest.storage.tablish, 42 | }); 43 | const chunkPrimaryKeys = new Set( 44 | // @ts-ignore 45 | chunk.map((c) => c.data[this.edge.source.primaryKey]) 46 | ); 47 | yield all.filter((x) => { 48 | return chunkPrimaryKeys.has(x.data[this.edge.destField]); 49 | }); 50 | break; 51 | case "junction": 52 | throw new Error("Junction memory hops not yet implemented"); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ts/packages/query/src/memory/MemoryHopExpression.ts: -------------------------------------------------------------------------------- 1 | import { IPersistedModel, specToDatasetKey } from "@vlcn.io/model-persisted"; 2 | import { EdgeSpec } from "@vlcn.io/schema-api"; 3 | import { ChunkIterable } from "../ChunkIterable.js"; 4 | import { HopExpression } from "../Expression.js"; 5 | import HopPlan from "../HopPlan.js"; 6 | import { IPlan } from "../Plan.js"; 7 | import MemoryHopChunkIterable from "./MemoryHopChunkIterable.js"; 8 | import { HoistedOperations } from "./MemorySourceExpression.js"; 9 | 10 | export default class MemoryHopExpression, TOut> 11 | implements HopExpression 12 | { 13 | constructor(public readonly edge: EdgeSpec, private ops: HoistedOperations) {} 14 | 15 | chainAfter(iterable: ChunkIterable): ChunkIterable { 16 | // We have an implicit join condition going on. 17 | // "chain after" will have roots populated from "iterable" 18 | return new MemoryHopChunkIterable(this.edge, this.ops, iterable); 19 | } 20 | 21 | /** 22 | * Optimizes the current plan (plan) and folds in the next hop (nextHop) if possible. 23 | * SourcePlan is retained in case we need to chain after source. 24 | */ 25 | optimize(sourcePlan: IPlan, plan: HopPlan, nextHop?: HopPlan): HopPlan { 26 | // TODO: commonize with `MemorySourceExpression` 27 | // const [hoistedExpressions, remainingExpressions] = this.hoist(plan, nextHop); 28 | let derivs = [...plan.derivations]; 29 | if (nextHop) { 30 | derivs.push(nextHop.hop); 31 | derivs = derivs.concat(nextHop.derivations); 32 | } 33 | return new HopPlan( 34 | sourcePlan, 35 | new MemoryHopExpression(this.edge, this.ops), 36 | derivs 37 | ); 38 | // return plan; 39 | } 40 | 41 | type: "hop" = "hop"; 42 | 43 | get destSpec() { 44 | return this.edge.dest; 45 | } 46 | 47 | implicatedDataset(): string { 48 | return specToDatasetKey(this.destSpec); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ts/packages/query/src/memory/MemoryHopQuery.ts: -------------------------------------------------------------------------------- 1 | import { HopExpression } from "../Expression.js"; 2 | import { HopQuery, Query } from "../Query.js"; 3 | import { EdgeSpec } from "@vlcn.io/schema-api"; 4 | import { IPersistedModel } from "@vlcn.io/model-persisted"; 5 | import MemoryHopExpression from "./MemoryHopExpression.js"; 6 | 7 | export default class MemoryHopQuery< 8 | TIn extends IPersistedModel, 9 | TOut 10 | > extends HopQuery { 11 | static create, TOut>( 12 | sourceQuery: Query, 13 | edge: EdgeSpec 14 | ) { 15 | // source could be anything. 16 | // dest is memory. 17 | // standalone edge could be memory or sql... 18 | return new MemoryHopQuery( 19 | sourceQuery, 20 | new MemoryHopExpression(edge, { what: "model" }) 21 | ); 22 | } 23 | } 24 | 25 | function createChainedHopExpression( 26 | edge: EdgeSpec 27 | ): HopExpression { 28 | throw new Error("In memory hop not yet supported"); 29 | } 30 | -------------------------------------------------------------------------------- /ts/packages/query/src/memory/MemorySourceChunkIterable.ts: -------------------------------------------------------------------------------- 1 | import { BaseChunkIterable } from "../ChunkIterable.js"; 2 | import { invariant } from "@vlcn.io/util"; 3 | import { config, MemoryReadQuery, MemoryResolvedDB } from "@vlcn.io/config"; 4 | import { JunctionEdgeSpec, NodeSpec } from "@vlcn.io/schema-api"; 5 | import { IPersistedModel } from "@vlcn.io/model-persisted"; 6 | 7 | export default class MemorySourceChunkIterable< 8 | T extends IPersistedModel 9 | > extends BaseChunkIterable { 10 | constructor( 11 | private spec: NodeSpec | JunctionEdgeSpec, 12 | private query: MemoryReadQuery 13 | ) { 14 | super(); 15 | invariant( 16 | this.spec.storage.type === "memory", 17 | "Memory source used for non-memory model!" 18 | ); 19 | } 20 | 21 | async *[Symbol.asyncIterator](): AsyncIterator { 22 | // TODO: stronger types one day 23 | // e.g., exec should by parametrized and checked against T somehow. 24 | // Should probably allow a namespace too? 25 | // also... this is pretty generic and would apply to non-sql data sources too. 26 | // given the actual query execution happens in the resolver. 27 | // also -- should we chunk it at all? 28 | const resolvedDb = config.storage( 29 | this.spec.storage.engine, 30 | this.spec.storage.db 31 | ) as MemoryResolvedDB; 32 | yield await resolvedDb.read(this.query); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ts/packages/query/src/memory/MemorySourceExpression.ts: -------------------------------------------------------------------------------- 1 | import { SourceExpression } from "../Expression.js"; 2 | import Plan from "../Plan.js"; 3 | import { ChunkIterable } from "../ChunkIterable.js"; 4 | import HopPlan from "../HopPlan.js"; 5 | import { 6 | BasePersistedModelData, 7 | IPersistedModel, 8 | specToDatasetKey, 9 | } from "@vlcn.io/model-persisted"; 10 | import { JunctionEdgeSpec, NodeSpec } from "@vlcn.io/schema-api"; 11 | import MemorySourceChunkIterable from "./MemorySourceChunkIterable.js"; 12 | 13 | export interface SQLResult {} 14 | 15 | export type HoistedOperations = { 16 | what: "model" | "ids" | "edges" | "count"; 17 | roots?: any[]; 18 | }; 19 | export default class MemorySourceExpression< 20 | T extends IPersistedModel 21 | > implements SourceExpression 22 | { 23 | constructor( 24 | // we should take a schema instead of db 25 | // we'd need the schema to know if we can hoist certain fields or not 26 | public readonly spec: NodeSpec | JunctionEdgeSpec, 27 | private ops: HoistedOperations 28 | ) {} 29 | 30 | optimize(plan: Plan, nextHop?: HopPlan): Plan { 31 | // TOOD: in-memory hoisting 32 | // we should iterate our expressions and see which ones can be hoisted 33 | // e.g., id filters. 34 | // We'd need to hoist roots. That's about it... 35 | // We could not even hoist and rely on later expressions 36 | // const [hoistedExpressions, remainingExpressions] = this.hoist(plan, nextHop); 37 | let derivs = [...plan.derivations]; 38 | if (nextHop) { 39 | derivs.push(nextHop.hop); 40 | derivs = derivs.concat(nextHop.derivations); 41 | } 42 | return new Plan(new MemorySourceExpression(this.spec, this.ops), derivs); 43 | // return plan; 44 | } 45 | 46 | get iterable(): ChunkIterable { 47 | return new MemorySourceChunkIterable(this.spec, { 48 | type: "read", 49 | tablish: this.spec.storage.tablish, 50 | roots: this.ops.roots, 51 | }); 52 | } 53 | 54 | implicatedDataset(): string { 55 | return specToDatasetKey(this.spec); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ts/packages/query/src/memory/MemorySourceQuery.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BasePersistedModelData, 3 | IPersistedModel, 4 | ModelSpecWithCreate, 5 | } from "@vlcn.io/model-persisted"; 6 | import { SourceQuery } from "../Query.js"; 7 | import MemorySourceExpression from "./MemorySourceExpression.js"; 8 | 9 | export default class MemorySourceQuery< 10 | T extends IPersistedModel 11 | > extends SourceQuery { 12 | constructor(spec: ModelSpecWithCreate) { 13 | super(new MemorySourceExpression(spec, { what: "model" })); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ts/packages/query/src/sql/SQLHopChunkIterable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SQLHopChunkIterable is needed to correctly handle an input source of ids to the query. 3 | * Basically like SQLSourceChunkIterable except "specAndOpsToQuery" would receive a set of source ids 4 | * from which the hop starts. 5 | * 6 | * HopChunkIterables are only used when a hop could not be rolled into a source expression. 7 | * As such, the hop chunk iterable receives a set of ids that represent the set of nodes being hopped to. 8 | * 9 | * What does this look like? 10 | * - Field edges 11 | * - IDs contained in the fields are provided to the hop iterable 12 | * - `where B.id IN (_input ids_)` 13 | * - FK edges 14 | * - IDs of the prior nodes are provided to the hop iterable which then crafts a 15 | * - `where B.fk IN (_input ids_)` 16 | * - Followed Jx edges 17 | * - ID2s provided 18 | * - Non followed jx edges 19 | * - ID1s provided...... this means the hop iterable output is an edge? 20 | * - Inverse jx edges 21 | * - Non followed inverse jx edges 22 | */ 23 | 24 | import { EdgeSpec } from "@vlcn.io/schema-api"; 25 | import { BaseChunkIterable } from "../ChunkIterable.js"; 26 | import { HoistedOperations } from "./SQLExpression.js"; 27 | 28 | export default class SQLHopChunkIterable extends BaseChunkIterable { 29 | constructor(private edge: EdgeSpec, private ops: HoistedOperations) { 30 | super(); 31 | } 32 | 33 | async *[Symbol.asyncIterator](): AsyncIterator { 34 | throw new Error( 35 | "Unimplemented -- see comments at the top of this file for implementation path" 36 | ); 37 | // yield await specAndOpsToQuery(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ts/packages/query/src/sql/SQLHopExpression.ts: -------------------------------------------------------------------------------- 1 | import { specToDatasetKey } from "@vlcn.io/model-persisted"; 2 | import { EdgeSpec } from "@vlcn.io/schema-api"; 3 | import { ChunkIterable } from "../ChunkIterable.js"; 4 | import { HopExpression } from "../Expression.js"; 5 | import HopPlan from "../HopPlan.js"; 6 | import { IPlan } from "../Plan.js"; 7 | import SQLExpression, { HoistedOperations } from "./SQLExpression.js"; 8 | import SQLHopChunkIterable from "./SQLHopChunkIterable.js"; 9 | 10 | export default class SQLHopExpression 11 | extends SQLExpression 12 | implements HopExpression 13 | { 14 | constructor(public readonly edge: EdgeSpec, ops: HoistedOperations) { 15 | super(ops); 16 | } 17 | 18 | chainAfter(iterable: ChunkIterable): ChunkIterable { 19 | return new SQLHopChunkIterable(this.edge, this.ops); 20 | } 21 | 22 | /** 23 | * Optimizes the current plan (plan) and folds in the next hop (nextHop) if possible. 24 | * SourcePlan is retained in case we need to chain after source. 25 | */ 26 | optimize(sourcePlan: IPlan, plan: HopPlan, nextHop?: HopPlan): HopPlan { 27 | const [hoistedExpressions, remainingExpressions] = this.hoist( 28 | plan, 29 | nextHop 30 | ); 31 | return new HopPlan( 32 | sourcePlan, 33 | new SQLHopExpression(this.edge, hoistedExpressions), 34 | remainingExpressions 35 | ); 36 | } 37 | 38 | type: "hop" = "hop"; 39 | 40 | get destSpec() { 41 | return this.edge.dest; 42 | } 43 | 44 | implicatedDataset(): string { 45 | return specToDatasetKey(this.destSpec); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ts/packages/query/src/sql/SQLHopQuery.ts: -------------------------------------------------------------------------------- 1 | import { invariant } from "@vlcn.io/util"; 2 | import { HopExpression } from "../Expression.js"; 3 | import { HopQuery, Query } from "../Query.js"; 4 | import { EdgeSpec } from "@vlcn.io/schema-api"; 5 | import SQLHopExpression from "./SQLHopExpression.js"; 6 | 7 | export default class SQLHopQuery extends HopQuery { 8 | /* 9 | A SQL hop query means that the next thing is SQL backed. 10 | We'll take source and see what the source is to determine what HOP 11 | expression to construct? 12 | */ 13 | static create(sourceQuery: Query, edge: EdgeSpec) { 14 | // based on source and dest spec, determine the appropriate hop expression 15 | if (edge.source.storage.type === "sql") { 16 | // Is the query layer doing this correctly? 17 | invariant( 18 | edge.dest.storage.type === "sql", 19 | "SQLHopQuery created for non-sql destination" 20 | ); 21 | 22 | // If we're the same storage on the same DB, we can use a join expression 23 | if (edge.source.storage.db === edge.dest.storage.db) { 24 | return new SQLHopQuery( 25 | sourceQuery, 26 | new SQLHopExpression(edge, { what: "model" }) 27 | ); 28 | } 29 | } 30 | return new SQLHopQuery( 31 | sourceQuery, 32 | createChainedHopExpression(edge) 33 | ); 34 | } 35 | } 36 | 37 | function createChainedHopExpression( 38 | edge: EdgeSpec 39 | ): HopExpression { 40 | throw new Error("In memory hop not yet supported"); 41 | } 42 | -------------------------------------------------------------------------------- /ts/packages/query/src/sql/SQLSourceExpression.ts: -------------------------------------------------------------------------------- 1 | import { SourceExpression } from "../Expression.js"; 2 | import SQLSourceChunkIterable from "./SQLSourceChunkIterable.js"; 3 | import Plan from "../Plan.js"; 4 | import { ChunkIterable } from "../ChunkIterable.js"; 5 | import HopPlan from "../HopPlan.js"; 6 | import { 7 | BasePersistedModelData, 8 | IPersistedModel, 9 | specToDatasetKey, 10 | } from "@vlcn.io/model-persisted"; 11 | import SQLExpression, { HoistedOperations } from "./SQLExpression.js"; 12 | import { JunctionEdgeSpec, NodeSpec } from "@vlcn.io/schema-api"; 13 | 14 | export interface SQLResult {} 15 | 16 | export default class SQLSourceExpression< 17 | T extends IPersistedModel 18 | > 19 | extends SQLExpression 20 | implements SourceExpression 21 | { 22 | constructor( 23 | // we should take a schema instead of db 24 | // we'd need the schema to know if we can hoist certain fields or not 25 | public readonly spec: NodeSpec | JunctionEdgeSpec, 26 | ops: HoistedOperations 27 | ) { 28 | super(ops); 29 | } 30 | 31 | optimize(plan: Plan, nextHop?: HopPlan): Plan { 32 | const [hoistedExpressions, remainingExpressions] = this.hoist( 33 | plan, 34 | nextHop 35 | ); 36 | return new Plan( 37 | new SQLSourceExpression(this.spec, hoistedExpressions), 38 | remainingExpressions 39 | ); 40 | } 41 | 42 | get iterable(): ChunkIterable { 43 | return new SQLSourceChunkIterable(this.spec, this.ops); 44 | } 45 | 46 | implicatedDataset(): string { 47 | return specToDatasetKey(this.spec); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ts/packages/query/src/sql/SQLSourceQuery.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BasePersistedModelData, 3 | IPersistedModel, 4 | ModelSpecWithCreate, 5 | } from "@vlcn.io/model-persisted"; 6 | import { SourceQuery } from "../Query.js"; 7 | import SQLSourceExpression from "./SQLSourceExpression.js"; 8 | 9 | export default class SQLSourceQuery< 10 | T extends IPersistedModel 11 | > extends SourceQuery { 12 | constructor(spec: ModelSpecWithCreate) { 13 | super(new SQLSourceExpression(spec, { what: "model" })); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ts/packages/query/src/sql/__tests__/specAndOpsToQuery.test.ts: -------------------------------------------------------------------------------- 1 | test('TEST TODO', () => {}); 2 | -------------------------------------------------------------------------------- /ts/packages/query/src/trace.ts: -------------------------------------------------------------------------------- 1 | import { tracer, Tracer } from "@vlcn.io/instrument"; 2 | 3 | const t: Tracer = tracer("@vlcn.io/query", "0.2.3"); 4 | 5 | export default t; 6 | -------------------------------------------------------------------------------- /ts/packages/query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [ 9 | { "path": "../model-persisted" }, 10 | { "path": "../schema-api" }, 11 | { "path": "../sql" }, 12 | { "path": "../config" }, 13 | { "path": "../instrument" } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /ts/packages/react/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | -------------------------------------------------------------------------------- /ts/packages/react/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/packages/react/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | targets: { 7 | node: "current", 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /ts/packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/model-react", 3 | "version": "2.0.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/react" 10 | }, 11 | "dependencies": { 12 | "@strut/counter": "^0.0.11", 13 | "suspend-react": "^0.0.8" 14 | }, 15 | "peerDependencies": { 16 | "@vlcn.io/runtime": "^0.4.0", 17 | "react": "^18.2.0" 18 | }, 19 | "devDependencies": { 20 | "@vlcn.io/runtime": "workspace:*", 21 | "@babel/core": "^7.18.13", 22 | "@babel/preset-env": "^7.18.10", 23 | "@types/jest": "^28.1.8", 24 | "@types/react": "^18.0.17", 25 | "@typescript-eslint/typescript-estree": "^5.35.1", 26 | "jest": "^29.0.1", 27 | "react": "^18.2.0", 28 | "typescript": "^4.8.2" 29 | }, 30 | "scripts": { 31 | "clean": "tsc --build --clean", 32 | "build": "tsc --build", 33 | "watch": "tsc --build -w", 34 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true", 35 | "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js" 36 | }, 37 | "jest": { 38 | "testMatch": [ 39 | "**/__tests__/**/*.test.js", 40 | "**/__tests__/**/*.test.jsx" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ts/packages/react/src/__tests__/cache.test.ts: -------------------------------------------------------------------------------- 1 | import { QueryCache } from '../hooks.js'; 2 | 3 | test('evicts at size limit', () => { 4 | const cache = new QueryCache(2); 5 | 6 | for (let i = 0; i < 10; ++i) { 7 | cache.set(i + '', []); 8 | expect(cache.size).toBeLessThanOrEqual(2); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /ts/packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | export { useBind, useQuery, UseQueryData, UseQueryOptions, useLiveResult } from './hooks.js'; 2 | export { createHooks } from './createHooks.js'; 3 | -------------------------------------------------------------------------------- /ts/packages/react/src/useSync.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | type Resolution = { 4 | loading: boolean; 5 | error?: any; 6 | data?: T; 7 | }; 8 | 9 | // TODO: update to use eager promises. 10 | // Note -- could you technically unify `useSync` and `useQuery`? 11 | // Probably... if `query->gen` did caching.... 12 | export default function useSync(fn: () => Promise, deps: any[] = []): Resolution { 13 | const [resolution, setResolution] = useState>({ 14 | loading: true, 15 | }); 16 | useEffect(() => { 17 | fn() 18 | .then(r => { 19 | setResolution({ 20 | loading: false, 21 | data: r, 22 | }); 23 | }) 24 | .catch(e => { 25 | setResolution({ 26 | loading: false, 27 | error: e, 28 | }); 29 | }); 30 | }, []); 31 | return resolution; 32 | } 33 | -------------------------------------------------------------------------------- /ts/packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src", 6 | "jsx": "react" 7 | }, 8 | "include": ["./src/"], 9 | "references": [{ "path": "../runtime" }] 10 | } 11 | -------------------------------------------------------------------------------- /ts/packages/runtime/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /ts/packages/runtime/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/packages/runtime/README.md: -------------------------------------------------------------------------------- 1 | Single package for all typescript runtime components. For simpler devex for the end user. -------------------------------------------------------------------------------- /ts/packages/runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/runtime", 3 | "version": "0.4.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/runtime" 10 | }, 11 | "dependencies": { 12 | "@vlcn.io/cache": "workspace:*", 13 | "@vlcn.io/config": "workspace:*", 14 | "@vlcn.io/migration": "workspace:*", 15 | "@vlcn.io/model-persisted": "workspace:*", 16 | "@vlcn.io/query": "workspace:*", 17 | "@vlcn.io/sql": "workspace:*", 18 | "@vlcn.io/id": "workspace:*", 19 | "@vlcn.io/util": "workspace:*", 20 | "@vlcn.io/value": "workspace:*" 21 | }, 22 | "devDependencies": { 23 | "typescript": "^4.8.2" 24 | }, 25 | "scripts": { 26 | "clean": "tsc --build --clean", 27 | "build": "tsc --build", 28 | "watch": "tsc --build -w", 29 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ts/packages/runtime/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DBResolver, SQLResolvedDB } from "@vlcn.io/config"; 2 | 3 | export * from "@vlcn.io/config"; 4 | export * from "@vlcn.io/model-persisted"; 5 | export * from "@vlcn.io/query"; 6 | export * from "@vlcn.io/id"; 7 | export * from "@vlcn.io/migration"; 8 | export { newId } from "@vlcn.io/id"; 9 | export { default as Cache } from "@vlcn.io/cache"; 10 | export * from "@vlcn.io/sql"; 11 | 12 | export function basicSqliteResolver( 13 | dbName: string, 14 | connection: SQLResolvedDB 15 | ): DBResolver { 16 | return { 17 | storage: (engine, db) => { 18 | if (engine !== "sqlite") { 19 | throw new Error( 20 | "Tried getting a sqlite connection while specifying engine " + engine 21 | ); 22 | } 23 | if (db !== dbName) { 24 | throw new Error( 25 | "Tried getting db named " + db + " but we only know about " + dbName 26 | ); 27 | } 28 | return connection; 29 | }, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /ts/packages/runtime/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [ 9 | { "path": "../cache" }, 10 | { "path": "../config" }, 11 | { "path": "../id" }, 12 | { "path": "../model-persisted" }, 13 | { "path": "../query" }, 14 | { "path": "../migration" }, 15 | { "path": "../sql" }, 16 | { "path": "../value" } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /ts/packages/schema-api/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /ts/packages/schema-api/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/packages/schema-api/.pnpm-debug.log: -------------------------------------------------------------------------------- 1 | { 2 | "0 debug pnpm:scope": { 3 | "selected": 1, 4 | "workspacePrefix": "/Users/tantaman/workspace/aphrodite" 5 | }, 6 | "1 error pnpm": { 7 | "code": "ELIFECYCLE", 8 | "errno": "ENOENT", 9 | "syscall": "spawn", 10 | "file": "sh", 11 | "pkgid": "@vlcn.io/schema-api@0.0.1", 12 | "stage": "build", 13 | "script": "rm -rf ./lib && tsc", 14 | "pkgname": "@vlcn.io/schema-api", 15 | "err": { 16 | "name": "pnpm", 17 | "message": "@vlcn.io/schema-api@0.0.1 build: `rm -rf ./lib && tsc`\nspawn ENOENT", 18 | "code": "ELIFECYCLE", 19 | "stack": "pnpm: @aphro/schema-api@0.0.1 build: `rm -rf ./lib && tsc`\nspawn ENOENT\n at ChildProcess. (/Users/tantaman/.nvm/versions/node/v17.5.0/pnpm-global/5/node_modules/.pnpm/pnpm@6.32.4/node_modules/pnpm/dist/pnpm.cjs:92290:22)\n at ChildProcess.emit (node:events:526:28)\n at maybeClose (node:internal/child_process:1090:16)\n at Process.ChildProcess._handle.onexit (node:internal/child_process:302:5)" 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /ts/packages/schema-api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @aphro/schema-api 2 | 3 | ## 0.3.0 4 | 5 | ### Minor Changes 6 | 7 | - publish for testing 8 | 9 | ## 0.2.6 10 | 11 | ### Patch Changes 12 | 13 | - Export queries and specs, move connectors to own packages, fix #43 and other bugs 14 | 15 | ## 0.2.5 16 | 17 | ### Patch Changes 18 | 19 | - transaction support 20 | 21 | ## 0.2.4 22 | 23 | ### Patch Changes 24 | 25 | - Strict mode for typescript, useEffect vs useSyncExternalStore, useLiveResult hook 26 | 27 | ## 0.2.3 28 | 29 | ### Patch Changes 30 | 31 | - rebuild -- last publish had a clobbered version of pnpm 32 | 33 | ## 0.2.2 34 | 35 | ### Patch Changes 36 | 37 | - workaround to adhere to strict mode in generated code #43 38 | 39 | ## 0.2.1 40 | 41 | ### Patch Changes 42 | 43 | - generate bootstrapping utilities 44 | 45 | ## 0.2.0 46 | 47 | ### Minor Changes 48 | 49 | - Simplify manual files, change output dir for generated code, allow caching in live queries, simplify 1 to 1 edge fetches 50 | 51 | ## 0.1.4 52 | 53 | ### Patch Changes 54 | 55 | - update dependency on strut/utils, enable manual methods for models 56 | 57 | ## 0.1.3 58 | 59 | ### Patch Changes 60 | 61 | - enable nested collections of nodes 62 | 63 | ## 0.1.2 64 | 65 | ### Patch Changes 66 | 67 | - allow ephemeral nodes. allow type expressions for fields. 68 | 69 | ## 0.1.1 70 | 71 | ### Patch Changes 72 | 73 | - in-memory model support 74 | 75 | ## 0.1.0 76 | 77 | ### Minor Changes 78 | 79 | - Support for standalone / junction edges 80 | 81 | ## 0.0.11 82 | 83 | ### Patch Changes 84 | 85 | - count/orderBy/take implementation, support for NOT NULL, empty queries 86 | 87 | ## 0.0.10 88 | 89 | ### Patch Changes 90 | 91 | - Fix casing errors on filesystem 92 | 93 | ## 0.0.9 94 | 95 | ### Patch Changes 96 | 97 | - graphql support, 'create table if not exists' for easier bootstrapping, @databases connection support 98 | 99 | ## 0.0.8 100 | 101 | ### Patch Changes 102 | 103 | - full todomvc example, no partiall generated mutators, removal of knexjs 104 | 105 | ## 0.0.7 106 | 107 | ### Patch Changes 108 | 109 | - enable running in the browser, implement reactive queries 110 | 111 | ## 0.0.6 112 | 113 | ### Patch Changes 114 | 115 | - Simplify interactions with changesets, get basic hop queries working 116 | -------------------------------------------------------------------------------- /ts/packages/schema-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/schema-api", 3 | "version": "0.3.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/schema-api" 10 | }, 11 | "devDependencies": { 12 | "typescript": "^4.8.2" 13 | }, 14 | "scripts": { 15 | "clean": "tsc --build --clean", 16 | "build": "tsc --build", 17 | "watch": "tsc --build -w", 18 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 19 | }, 20 | "jest": { 21 | "testMatch": [ 22 | "**/__tests__/**/*.test.js" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ts/packages/schema-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"] 8 | } 9 | -------------------------------------------------------------------------------- /ts/packages/schema/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /ts/packages/schema/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/packages/schema/README.md: -------------------------------------------------------------------------------- 1 | # Schema 2 | 3 | The schema definition language and compiler. -------------------------------------------------------------------------------- /ts/packages/schema/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | targets: { 7 | node: "current", 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /ts/packages/schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/schema", 3 | "version": "0.4.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/schema" 10 | }, 11 | "dependencies": { 12 | "@vlcn.io/codegen-api": "workspace:*", 13 | "@vlcn.io/grammar-extension-api": "workspace:*", 14 | "@vlcn.io/schema-api": "workspace:*", 15 | "@vlcn.io/util": "workspace:*" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.18.13", 19 | "@babel/preset-env": "^7.18.10", 20 | "@types/jest": "^28.1.8", 21 | "@types/node": "^18.7.13", 22 | "@types/prettier": "^2.7.0", 23 | "@typescript-eslint/typescript-estree": "^5.35.1", 24 | "chalk": "^5.0.1", 25 | "command-line-args": "^5.2.1", 26 | "command-line-usage": "^6.1.3", 27 | "jest": "^29.0.1", 28 | "md5": "^2.3.0", 29 | "ohm-js": "^16.4.0", 30 | "prettier": "^2.7.1", 31 | "sql-formatter": "^10.0.0", 32 | "typescript": "^4.8.2" 33 | }, 34 | "scripts": { 35 | "clean": "tsc --build --clean", 36 | "build": "tsc --build", 37 | "watch": "tsc --build -w", 38 | "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js", 39 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 40 | }, 41 | "jest": { 42 | "testMatch": [ 43 | "**/__tests__/**/*.test.js" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ts/packages/schema/src/__tests__/schemaFilePipeline.test.ts: -------------------------------------------------------------------------------- 1 | test('TDOO', () => {}); 2 | -------------------------------------------------------------------------------- /ts/packages/schema/src/compile.ts: -------------------------------------------------------------------------------- 1 | import condense from "./parser/condense.js"; 2 | import validate from "./validate.js"; 3 | import { createParser } from "./parser/parse.js"; 4 | import { 5 | SchemaFile, 6 | SchemaFileAst, 7 | ValidationError, 8 | } from "@vlcn.io/schema-api"; 9 | import { Config } from "./runtimeConfig.js"; 10 | 11 | export function createCompiler(config: Config = {}) { 12 | const parser = createParser(config); 13 | 14 | const condensors: Map any> = new Map(); 15 | config.grammarExtensions?.forEach((e) => { 16 | if (condensors.has(e.name)) { 17 | throw new Error( 18 | "Condensor already exists for a plugin with the name/symbol " + e.name 19 | ); 20 | } 21 | condensors.set(e.name, e.condensor); 22 | }); 23 | 24 | function compile(path: string): [ValidationError[], SchemaFile] { 25 | return compileFromAst(parser.parse(path)); 26 | } 27 | 28 | function compileFromString( 29 | contents: string 30 | ): [ValidationError[], SchemaFile] { 31 | const ast = parser.parseString(contents); 32 | return compileFromAst(ast); 33 | } 34 | 35 | function compileFromAst(ast: SchemaFileAst): [ValidationError[], SchemaFile] { 36 | const [condenseErrors, schemaFile] = condense(ast, condensors); 37 | 38 | const validationErrors = validate(schemaFile); 39 | 40 | return [[...condenseErrors, ...validationErrors], schemaFile]; 41 | } 42 | 43 | return { 44 | compile, 45 | compileFromString, 46 | compileFromAst, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /ts/packages/schema/src/field.ts: -------------------------------------------------------------------------------- 1 | import { FieldDeclaration, ID, TypeAtom } from "@vlcn.io/schema-api"; 2 | 3 | export default { 4 | decorate(f: FieldDeclaration, decoration: string) { 5 | const decorators = (f.decorators = f.decorators || []); 6 | decorators.push(decoration); 7 | }, 8 | isNullable(f: FieldDeclaration): boolean { 9 | return f.type.some( 10 | (t) => 11 | t === "null" || 12 | (typeof t !== "string" && 13 | t.type === "primitive" && 14 | t.subtype === "null") 15 | ); 16 | }, 17 | pullNamedTypesExcludingNull(f: FieldDeclaration): TypeAtom[] { 18 | return f.type.filter((t) => { 19 | if (typeof t === "string") { 20 | return t !== "null"; 21 | } 22 | if (t.type === "primitive") { 23 | return t.subtype !== "null"; 24 | } 25 | return t.type !== "intersection" && t.type !== "union"; 26 | }); 27 | }, 28 | removeNull(t: readonly TypeAtom[]): readonly TypeAtom[] { 29 | const nullIndex = t.findIndex((t) => { 30 | if (typeof t !== "string") { 31 | return t.type === "primitive" && t.subtype === "null"; 32 | } 33 | }); 34 | if (nullIndex < 0) { 35 | return t; 36 | } 37 | 38 | if (nullIndex === 0) { 39 | return t.slice(2); 40 | } else { 41 | return [...t.slice(0, nullIndex - 1), ...t.slice(nullIndex + 1)]; 42 | } 43 | }, 44 | pullNullType(f: FieldDeclaration): TypeAtom | undefined { 45 | return f.type.filter((t) => { 46 | if (typeof t === "string") { 47 | return t === "null"; 48 | } 49 | return t.type === "primitive" && t.subtype === "null"; 50 | })[0]; 51 | }, 52 | hasId(f: FieldDeclaration): boolean { 53 | return f.type.some((t) => typeof t !== "string" && t.type === "id"); 54 | }, 55 | getOnlyId(f: FieldDeclaration): ID { 56 | const ret = f.type.filter( 57 | (t): t is ID => typeof t !== "string" && t.type === "id" 58 | ); 59 | if (ret.length > 1) { 60 | throw new Error(`Multiple id types exist for ${f.name}`); 61 | } 62 | return ret[0]; 63 | }, 64 | isComplex, 65 | encoding(f: FieldDeclaration): "json" | "none" { 66 | return isComplex(f) ? "json" : "none"; 67 | }, 68 | }; 69 | 70 | function isComplex(f: FieldDeclaration): boolean { 71 | return f.type.some( 72 | (t) => typeof t === "string" || t.type === "array" || t.type === "map" 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /ts/packages/schema/src/index.ts: -------------------------------------------------------------------------------- 1 | export { createCompiler } from './compile.js'; 2 | export { stopsCodegen } from './validate.js'; 3 | export { default as nodeFn } from './node.js'; 4 | export { default as edgeFn } from './edge.js'; 5 | export { default as fieldFn } from './field.js'; 6 | export * from './module.js'; 7 | -------------------------------------------------------------------------------- /ts/packages/schema/src/module.ts: -------------------------------------------------------------------------------- 1 | import { Import } from "@vlcn.io/schema-api"; 2 | 3 | export function tsImport( 4 | name: string | null, 5 | as: string | null, 6 | from: string 7 | ): Import { 8 | return { 9 | name: name?.trim(), 10 | as, 11 | from, 12 | }; 13 | } 14 | 15 | export function javaImport(from: string): Import { 16 | return { 17 | from, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /ts/packages/schema/src/node.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Enum, 3 | Field, 4 | FieldDeclaration, 5 | ID, 6 | Import, 7 | SchemaEdge, 8 | SchemaNode, 9 | } from "@vlcn.io/schema-api"; 10 | import fieldFn from "./field.js"; 11 | 12 | const inboundEdges = { 13 | isForeignKeyEdge() {}, 14 | 15 | isFieldEdge() {}, 16 | 17 | isJunctionEdge() {}, 18 | }; 19 | 20 | const outboundEdges = { 21 | isForeignKeyEdge() {}, 22 | 23 | isFieldEdge() {}, 24 | 25 | isJunctionEdge() {}, 26 | }; 27 | 28 | const fields = {}; 29 | 30 | export default { 31 | allEdges(node: SchemaNode) { 32 | const inboundEdges = Object.values( 33 | node.extensions.inboundEdges?.edges || {} 34 | ); 35 | const outboundEdges = Object.values( 36 | node.extensions.outboundEdges?.edges || {} 37 | ); 38 | 39 | return [...inboundEdges, ...outboundEdges]; 40 | }, 41 | 42 | queryTypeName(nodeName: string): string { 43 | return nodeName + "Query"; 44 | }, 45 | 46 | addModuleImport(node: SchemaNode, imp: Import) { 47 | const module = (node.extensions.module = node.extensions.module || { 48 | name: "moduleConfig", 49 | imports: new Map(), 50 | }); 51 | 52 | module.imports.set(JSON.stringify(imp), imp); 53 | }, 54 | 55 | decorateType(node: SchemaNode, decoration: string) { 56 | const typeConfig = node.extensions.type || { 57 | name: "typeConfig", 58 | decorators: [], 59 | }; 60 | 61 | typeConfig.decorators?.push(decoration); 62 | }, 63 | 64 | primaryKey(node: SchemaNode): FieldDeclaration { 65 | // TODO: support different primary key fields at some point 66 | return node.fields.id; 67 | }, 68 | 69 | specName(nodeName: string, srcName?: string): string { 70 | return nodeName + "Spec"; 71 | }, 72 | 73 | inlineEnums(node: SchemaNode): Enum[] { 74 | return Object.values(node.fields) 75 | .flatMap((f) => f.type) 76 | .filter( 77 | (f): f is Enum => typeof f !== "string" && f.type === "enumeration" 78 | ); 79 | }, 80 | 81 | isRequiredField(node: SchemaNode, field: string): boolean { 82 | return !fieldFn.isNullable(node.fields[field]); 83 | }, 84 | 85 | tableName(node: SchemaNode | SchemaEdge): string { 86 | return node.name.toLowerCase(); 87 | }, 88 | }; 89 | -------------------------------------------------------------------------------- /ts/packages/schema/src/parser/__tests__/documentSchemaFile.ts: -------------------------------------------------------------------------------- 1 | export const contents = ` 2 | Application as UnmanagedNode { 3 | currentScreen: HomeScreen | ProfileScreen 4 | } 5 | 6 | HomeScreen as UnmanagedNode { 7 | 8 | } 9 | 10 | ProfileScreen as UnmanagedNode { 11 | 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /ts/packages/schema/src/parser/__tests__/documentType.test.ts: -------------------------------------------------------------------------------- 1 | import { createParser } from '../parse.js'; 2 | import { contents } from './documentSchemaFile'; 3 | 4 | const { parseString } = createParser(); 5 | test('Parsing with the compiled grammar', () => { 6 | const parsed = parseString(contents); 7 | expect(parsed).toEqual(ast); 8 | }); 9 | 10 | const ast = { 11 | preamble: {}, 12 | entities: [ 13 | { 14 | type: 'node', 15 | as: 'UnmanagedNode', 16 | name: 'Application', 17 | fields: [{ name: 'currentScreen', type: ['HomeScreen', { type: 'union' }, 'ProfileScreen'] }], 18 | extensions: [], 19 | }, 20 | { type: 'node', as: 'UnmanagedNode', name: 'HomeScreen', fields: [], extensions: [] }, 21 | { type: 'node', as: 'UnmanagedNode', name: 'ProfileScreen', fields: [], extensions: [] }, 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /ts/packages/schema/src/parser/__tests__/grammar.test.ts: -------------------------------------------------------------------------------- 1 | import { contents, ast } from './testSchemaFile.js'; 2 | import { compileGrammar } from '../ohm/grammar.js'; 3 | 4 | const grammar = compileGrammar(); 5 | 6 | test('parsing a small schema', () => { 7 | const match = grammar.match(contents); 8 | expect(match.succeeded()).toBe(true); 9 | }); 10 | -------------------------------------------------------------------------------- /ts/packages/schema/src/parser/__tests__/parse.test.ts: -------------------------------------------------------------------------------- 1 | import { createParser } from '../parse.js'; 2 | import { contents, ast } from './testSchemaFile.js'; 3 | 4 | const { parseString } = createParser(); 5 | test('Parsing with the compiled grammar', () => { 6 | const parsed = parseString(contents); 7 | // Getting a shit ton of output on this line when the test fails? 8 | // You probably forgot to call `.toAst` or `.sourceString` on some parameter in `parse.ts` 9 | expect(parsed).toEqual(ast); 10 | }); 11 | -------------------------------------------------------------------------------- /ts/packages/schema/src/parser/condenseEntities.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NodeReference, 3 | SchemaFileAst, 4 | ValidationError, 5 | } from "@vlcn.io/schema-api"; 6 | 7 | export default function condenseEntities( 8 | entities: { 9 | [key: string]: Ta; 10 | }, 11 | preamble: SchemaFileAst["preamble"], 12 | condensor: ( 13 | entity: Ta, 14 | preamble: SchemaFileAst["preamble"] 15 | ) => [ValidationError[], Tc] 16 | ): [ 17 | ValidationError[], 18 | { 19 | [key: string]: Tc; 20 | } 21 | ] { 22 | let errors: ValidationError[] = []; 23 | const condensedEntities: { [key: NodeReference]: Tc } = Object.entries( 24 | entities 25 | ).reduce((l: { [key: string]: Tc }, [key, entity]) => { 26 | const [entityErrors, condensed] = condensor(entity, preamble); 27 | errors = errors.concat(entityErrors); 28 | l[key] = condensed; 29 | return l; 30 | }, {}); 31 | return [errors, condensedEntities]; 32 | } 33 | -------------------------------------------------------------------------------- /ts/packages/schema/src/runtimeConfig.ts: -------------------------------------------------------------------------------- 1 | import { GrammarExtension } from "@vlcn.io/grammar-extension-api"; 2 | import { Step } from "@vlcn.io/codegen-api"; 3 | 4 | export type Config = { 5 | grammarExtensions?: GrammarExtension[]; 6 | codegenExtensions?: Step[]; 7 | }; 8 | -------------------------------------------------------------------------------- /ts/packages/schema/src/type.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlcn-io/model/7d3850fc5d1f0b2ed52d721edb898fccc21cb90f/ts/packages/schema/src/type.ts -------------------------------------------------------------------------------- /ts/packages/schema/src/validate.ts: -------------------------------------------------------------------------------- 1 | import { SchemaFile, ValidationError } from "@vlcn.io/schema-api"; 2 | 3 | export default function validate(schemaFile: SchemaFile): ValidationError[] { 4 | return []; 5 | } 6 | 7 | export function stopsCodegen(error: ValidationError): boolean { 8 | return error.severity === "error"; 9 | } 10 | 11 | /* 12 | - Validate imports 13 | - Validate id_of 14 | - Validate inbound and outbound edges 15 | 16 | SQL: 17 | - Validate indexing of fields (foreign key, junction) 18 | */ 19 | -------------------------------------------------------------------------------- /ts/packages/schema/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [ 9 | { "path": "../schema-api" }, 10 | { "path": "../grammar-extension-api" }, 11 | { "path": "../codegen-api" } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /ts/packages/sql/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /ts/packages/sql/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /ts/packages/sql/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @aphro/sql-ts 2 | 3 | ## 0.3.0 4 | 5 | ### Minor Changes 6 | 7 | - publish for testing 8 | 9 | ## 0.2.6 10 | 11 | ### Patch Changes 12 | 13 | - Export queries and specs, move connectors to own packages, fix #43 and other bugs 14 | 15 | ## 0.2.5 16 | 17 | ### Patch Changes 18 | 19 | - transaction support 20 | 21 | ## 0.2.4 22 | 23 | ### Patch Changes 24 | 25 | - Strict mode for typescript, useEffect vs useSyncExternalStore, useLiveResult hook 26 | 27 | ## 0.2.3 28 | 29 | ### Patch Changes 30 | 31 | - rebuild -- last publish had a clobbered version of pnpm 32 | 33 | ## 0.2.2 34 | 35 | ### Patch Changes 36 | 37 | - workaround to adhere to strict mode in generated code #43 38 | 39 | ## 0.2.1 40 | 41 | ### Patch Changes 42 | 43 | - generate bootstrapping utilities 44 | 45 | ## 0.2.0 46 | 47 | ### Minor Changes 48 | 49 | - Simplify manual files, change output dir for generated code, allow caching in live queries, simplify 1 to 1 edge fetches 50 | 51 | ## 0.1.3 52 | 53 | ### Patch Changes 54 | 55 | - update dependency on strut/utils, enable manual methods for models 56 | 57 | ## 0.1.2 58 | 59 | ### Patch Changes 60 | 61 | - allow ephemeral nodes. allow type expressions for fields. 62 | 63 | ## 0.1.1 64 | 65 | ### Patch Changes 66 | 67 | - in-memory model support 68 | 69 | ## 0.1.0 70 | 71 | ### Minor Changes 72 | 73 | - Support for standalone / junction edges 74 | 75 | ## 0.0.6 76 | 77 | ### Patch Changes 78 | 79 | - count/orderBy/take implementation, support for NOT NULL, empty queries 80 | 81 | ## 0.0.5 82 | 83 | ### Patch Changes 84 | 85 | - Fix casing errors on filesystem 86 | 87 | ## 0.0.4 88 | 89 | ### Patch Changes 90 | 91 | - graphql support, 'create table if not exists' for easier bootstrapping, @databases connection support 92 | 93 | ## 0.0.3 94 | 95 | ### Patch Changes 96 | 97 | - full todomvc example, no partiall generated mutators, removal of knexjs 98 | 99 | ## 0.0.2 100 | 101 | ### Patch Changes 102 | 103 | - enable running in the browser, implement reactive queries 104 | -------------------------------------------------------------------------------- /ts/packages/sql/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /ts/packages/sql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/sql", 3 | "version": "0.3.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/sql" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.18.13", 13 | "@babel/preset-env": "^7.18.10", 14 | "@types/jest": "^28.1.8", 15 | "fast-check": "^3.1.2", 16 | "jest": "^29.0.1", 17 | "typescript": "^4.8.2" 18 | }, 19 | "scripts": { 20 | "clean": "tsc --build --clean", 21 | "build": "tsc --build", 22 | "watch": "tsc --build -w", 23 | "test": "node ./node_modules/jest/bin/jest.js", 24 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 25 | }, 26 | "jest": { 27 | "testMatch": [ 28 | "**/__tests__/**/*.test.js" 29 | ] 30 | }, 31 | "dependencies": { 32 | "@databases/escape-identifier": "^1.0.3", 33 | "@databases/sql": "^3.2.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ts/packages/sql/src/index.ts: -------------------------------------------------------------------------------- 1 | export { SQLQuery, sql, formatters } from './sql.js'; 2 | -------------------------------------------------------------------------------- /ts/packages/sql/src/sql.ts: -------------------------------------------------------------------------------- 1 | import { escapePostgresIdentifier, escapeSQLiteIdentifier } from '@databases/escape-identifier'; 2 | import { FormatConfig } from '@databases/sql'; 3 | export { default as sql, SQLQuery } from '@databases/sql'; 4 | 5 | const pgFormat: FormatConfig = { 6 | escapeIdentifier: str => escapePostgresIdentifier(str), 7 | formatValue: (value, index) => ({ placeholder: `$${index + 1}`, value }), 8 | }; 9 | 10 | const sqliteFormat: FormatConfig = { 11 | escapeIdentifier: str => escapeSQLiteIdentifier(str), 12 | formatValue: value => ({ placeholder: '?', value }), 13 | }; 14 | 15 | const error = { 16 | escapeIdentifier: (str: string) => { 17 | throw new Error('Memory storage should not go through sql formatting'); 18 | }, 19 | formatValue: (value: unknown) => { 20 | throw new Error('Memory storage should not go through sql formatting'); 21 | }, 22 | } as const; 23 | 24 | export const formatters: { [key in 'sqlite' | 'postgres' | 'memory' | 'ephemeral']: FormatConfig } = 25 | { 26 | sqlite: sqliteFormat, 27 | postgres: pgFormat, 28 | ephemeral: error, 29 | memory: error, 30 | } as const; 31 | -------------------------------------------------------------------------------- /ts/packages/sql/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [] 9 | } 10 | -------------------------------------------------------------------------------- /ts/packages/sync/notes.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlcn-io/model/7d3850fc5d1f0b2ed52d721edb898fccc21cb90f/ts/packages/sync/notes.md -------------------------------------------------------------------------------- /ts/packages/util/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @vlcn.io/util 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - publish for testing 8 | 9 | ## 0.0.2 10 | 11 | ### Patch Changes 12 | 13 | - retryable transactions, serializable transactions, conflict detection 14 | -------------------------------------------------------------------------------- /ts/packages/util/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /ts/packages/util/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/util", 3 | "version": "0.1.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/util" 10 | }, 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "@babel/core": "^7.18.13", 14 | "@babel/preset-env": "^7.18.10", 15 | "@types/jest": "^28.1.8", 16 | "fast-check": "^3.1.2", 17 | "jest": "^29.0.1", 18 | "typescript": "^4.8.2" 19 | }, 20 | "scripts": { 21 | "clean": "tsc --build --clean", 22 | "build": "tsc --build", 23 | "watch": "tsc --build -w", 24 | "test": "node ./node_modules/jest/bin/jest.js", 25 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 26 | }, 27 | "jest": { 28 | "testMatch": [ 29 | "**/__tests__/**/*.test.js" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ts/packages/util/src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { assertUnreachable, invariant, isHex } from "../index.js"; 2 | 3 | test("assert unreachable", () => { 4 | // @ts-expect-error -- test typechecker via this comment 5 | expect(() => assertUnreachable("foo")).toThrow(); 6 | }); 7 | 8 | test("invariant", () => { 9 | expect(() => invariant(false, "oops")).toThrow(); 10 | expect(() => invariant(true, "fixed")).not.toThrow(); 11 | }); 12 | 13 | // TODO: should fast-check this 14 | test("isHex", () => { 15 | expect(() => isHex("zdb")).toThrow(); 16 | expect(() => isHex("0abcdef123456789")).not.toThrow(); 17 | }); 18 | -------------------------------------------------------------------------------- /ts/packages/util/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as observe } from "./observe.js"; 2 | export { staticImplements } from "./static.js"; 3 | 4 | export function assertUnreachable(x: never): never { 5 | throw new Error("Didn't expect to get here"); 6 | } 7 | 8 | export function invariant(condition: boolean, message: string) { 9 | if (!condition) { 10 | throw new Error(message); 11 | } 12 | } 13 | 14 | const hexReg = /^[0-9A-Fa-f]+$/; 15 | export function isHex(h: string) { 16 | return hexReg.exec(h) != null; 17 | } 18 | 19 | export function noop() {} 20 | 21 | export function assert(condition: boolean) { 22 | if (!condition) { 23 | throw new Error("Assertion failed"); 24 | } 25 | } 26 | 27 | export function upcaseAt(str: string, i: number) { 28 | return str.substr(0, i) + str.substr(i, 1).toUpperCase() + str.substr(i + 1); 29 | } 30 | 31 | export function lowercaseAt(str: string, i: number) { 32 | return ( 33 | str.substring(0, i) + 34 | str.substring(i, 1).toLowerCase() + 35 | str.substring(i + 1) 36 | ); 37 | } 38 | 39 | export type Concat = string; 40 | 41 | export function falsish(x: any): boolean { 42 | return !!x === false; 43 | } 44 | 45 | export function isValidPropertyAccessor(a: string): boolean { 46 | return (a.match(/[A-z_$]+[A-z0-9_$]*/) || [])[0] === a; 47 | } 48 | 49 | export function not(x: any) { 50 | return !x; 51 | } 52 | 53 | export function asPropertyAccessor(a: string): string { 54 | return isValidPropertyAccessor(a) ? a : `'${a}'`; 55 | } 56 | -------------------------------------------------------------------------------- /ts/packages/util/src/observe.ts: -------------------------------------------------------------------------------- 1 | type It = { 2 | [Symbol.iterator]: () => It; 3 | throw(): { done: true; value: T }; 4 | return(): { done: true; value: T }; 5 | next(): { done: false; value: Promise }; 6 | }; 7 | 8 | export default function observe( 9 | initialize: (change: (x: T) => T) => null | (() => void) 10 | ) { 11 | let stale = false; 12 | let value: T; 13 | let resolve: ((value: T | PromiseLike) => void) | null; 14 | const dispose = initialize(change); 15 | 16 | function change(x: T) { 17 | if (resolve) resolve(x), (resolve = null); 18 | else stale = true; 19 | return (value = x); 20 | } 21 | 22 | function next(): { done: false; value: Promise } { 23 | return { 24 | done: false, 25 | value: stale 26 | ? ((stale = false), Promise.resolve(value)) 27 | : new Promise((_) => (resolve = _)), 28 | }; 29 | } 30 | 31 | const ret: It = { 32 | [Symbol.iterator]: () => ret, 33 | throw: () => (dispose != null && dispose(), { done: true, value }), 34 | return: () => (dispose != null && dispose(), { done: true, value }), 35 | next, 36 | }; 37 | return ret; 38 | } 39 | -------------------------------------------------------------------------------- /ts/packages/util/src/static.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/Microsoft/TypeScript/issues/13462#issuecomment-295685298 2 | 3 | interface Type { 4 | new (...args: any[]): T; 5 | } 6 | 7 | /* class decorator */ 8 | export function staticImplements() { 9 | return (constructor: T) => {}; 10 | } 11 | -------------------------------------------------------------------------------- /ts/packages/util/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [{}] 9 | } 10 | -------------------------------------------------------------------------------- /ts/packages/value/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @vlcn.io/value 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - publish for testing 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - @vlcn.io/util@0.1.0 13 | - @vlcn.io/zone@0.1.0 14 | 15 | ## 0.0.3 16 | 17 | ### Patch Changes 18 | 19 | - retryable transactions, serializable transactions, conflict detection 20 | - Updated dependencies 21 | - @vlcn.io/context-provider@0.0.2 22 | - @vlcn.io/util@0.0.2 23 | 24 | ## 0.0.2 25 | 26 | ### Patch Changes 27 | 28 | - include psd in observable value 29 | -------------------------------------------------------------------------------- /ts/packages/value/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /ts/packages/value/notes.md: -------------------------------------------------------------------------------- 1 | # Transaction Options 2 | 3 | Nested: 4 | 5 | - Commit just moves values to parent 6 | 7 | Async sibling transactions: 8 | 9 | - Serialize them? 10 | - Warn? 11 | - Fatal? 12 | - Alternate APIs: 13 | - serialTx 14 | - retryableTx 15 | - split sync tx into async and sync apis 16 | 17 | # Aphrodite Options 18 | 19 | - Normal ORM -- a write is a write, a read a read 20 | - Would need to serialize sibling transactions 21 | - lazy 22 | - still serialize siblings? 23 | - at end, we have a list of things to commit 24 | - lazyUnsafe 25 | - no serializing of siblings? 26 | 27 | # Bugs? 28 | 29 | - Dexie PSD doesn't seem to leave current zone correctly 30 | - needed to `.then.then` rather than just `.then` on the final result 31 | - Does it stay in zone after many async ops? 32 | -------------------------------------------------------------------------------- /ts/packages/value/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/value", 3 | "version": "0.1.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/value" 10 | }, 11 | "dependencies": { 12 | "typescript": "^4.8.2", 13 | "@vlcn.io/zone": "workspace:*", 14 | "@vlcn.io/util": "workspace:*" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "^7.18.13", 18 | "@babel/preset-env": "^7.18.10", 19 | "@types/jest": "^28.1.8", 20 | "fast-check": "^3.1.2", 21 | "jest": "^29.0.1" 22 | }, 23 | "peerDependencies": {}, 24 | "scripts": { 25 | "clean": "tsc --build --clean", 26 | "build": "tsc --build", 27 | "watch": "tsc --build -w", 28 | "test": "node ./node_modules/jest/bin/jest.js", 29 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 30 | }, 31 | "jest": { 32 | "testMatch": [ 33 | "**/__tests__/**/*.test.js" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ts/packages/value/src/History.ts: -------------------------------------------------------------------------------- 1 | import { MemoryVersion } from "./memory.js"; 2 | import { inflight, Transaction } from "./transaction.js"; 3 | 4 | type Node = { 5 | memVers: MemoryVersion; 6 | data: T; 7 | }; 8 | 9 | export class History { 10 | // most recent things are at the end 11 | private nodes: Node[] = []; 12 | 13 | at(memVers: MemoryVersion): T { 14 | for (let i = this.nodes.length - 1; i > -1; --i) { 15 | const node = this.nodes[i]; 16 | if (node.memVers <= memVers) { 17 | return node.data; 18 | } 19 | } 20 | 21 | throw new Error("Could not find any data for version " + memVers); 22 | } 23 | 24 | maybeAdd(data: T, memoryVersion: MemoryVersion): void { 25 | if (inflight.length === 0) { 26 | this.drop(); 27 | // if no tx is in flight we have no need to record history of values. 28 | // history of values is only retained for tx isolation. 29 | return; 30 | } 31 | 32 | // still inflight txs but our history is at a certain limit 33 | if (this.nodes.length > 3) { 34 | this.#maybeDrop(memoryVersion); 35 | } 36 | 37 | this.nodes.push({ 38 | memVers: memoryVersion, 39 | data, 40 | }); 41 | } 42 | 43 | public drop() { 44 | if (this.nodes.length > 0) { 45 | this.nodes = []; 46 | } 47 | } 48 | 49 | #maybeDrop(memoryVersion: MemoryVersion) { 50 | // TODO 51 | // based on min memory version of inflight transactions 52 | // We can drop all history entries except for the one before the one greater than the smallest inflight mem version. 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ts/packages/value/src/ObservableValue.ts: -------------------------------------------------------------------------------- 1 | // A value that provides hooks into its life cycle (pre-commit, commit) so we can 2 | // build triggers and observers. 3 | // Triggers being run pre-commit 4 | // Observers being run post-commit 5 | import { Event, IValue, Value } from "./Value.js"; 6 | import { PSD } from "@vlcn.io/zone"; 7 | import { Transaction } from "./transaction.js"; 8 | 9 | type OnTxComplete = (v: T, e: Event) => void; 10 | type Disposer = () => void; 11 | 12 | export interface IObservableValue extends IValue { 13 | onTransactionComplete(fn: OnTxComplete): Disposer; 14 | dispose(): void; 15 | // TODO: add an `onValueChange` that filters events if the value was set to the same value? 16 | } 17 | 18 | export class ObservableValue 19 | extends Value 20 | implements IObservableValue 21 | { 22 | #observers: Set<(v: T, e: Event) => void> = new Set(); 23 | 24 | onTransactionComplete(fn: OnTxComplete): Disposer { 25 | this.#observers.add(fn); 26 | return () => this.#observers.delete(fn); 27 | } 28 | 29 | dispose() { 30 | this.#observers.clear(); 31 | } 32 | 33 | __transactionComplete(e: Event) { 34 | this.#notifyObservers(e); 35 | } 36 | 37 | #notifyObservers(e: Event) { 38 | for (const o of this.#observers) { 39 | try { 40 | o(this.val, e); 41 | } catch (e) { 42 | console.error(e); 43 | } 44 | } 45 | } 46 | } 47 | 48 | export function observableValue(data: T): [IObservableValue, boolean] { 49 | const ret = new ObservableValue(data); 50 | 51 | // @ts-ignore 52 | const tx = PSD.tx as Transaction; 53 | let txComplete = false; 54 | if (tx) { 55 | tx.touch(ret, "create", data); 56 | } else { 57 | // we're not inside a running tx? then the tx is the create and is done. 58 | txComplete = true; 59 | ret.__transactionComplete("create"); 60 | } 61 | 62 | // we return a bool indicating if the tx is complete. 63 | // We do this so references to objects still being constructed do not escape. 64 | // e.g., it is common to create an observable value within the constructor of another object. 65 | // if the transaction completes during that object's construction, that object can be called back before 66 | // it's construction is complete. 67 | // if the object does not add a listener until the end of its construction in order to avoid this problem, 68 | // it can miss the transactionComplete callback. 69 | return [ret, txComplete]; 70 | } 71 | -------------------------------------------------------------------------------- /ts/packages/value/src/PersistedValue.ts: -------------------------------------------------------------------------------- 1 | import { memory, MemoryVersion } from "./memory.js"; 2 | import { IObservableValue, ObservableValue } from "./ObservableValue.js"; 3 | 4 | export interface IPersistedValue extends IObservableValue {} 5 | 6 | /** 7 | * On create we need to know: 8 | * - is this a hydration 9 | * - is this a new create 10 | * 11 | * If it is a hydration the caller should tell us. 12 | * If it is a hydration we need to: 13 | * 1. Check cache. If the thing is already in the cache, return that thing. 14 | * Should we update the cached thing if there is a delta? 15 | * 2. If the thing is not in the cache, return new item with mem vers set to min vers 16 | * 17 | * If not hydration, just create the thing as usual. Setting mem vers to next vers 18 | * 19 | * Our unique key for the cache for a value is: 20 | * db + collection + id 21 | */ 22 | class PersistedValue 23 | extends ObservableValue 24 | implements IPersistedValue 25 | { 26 | constructor(data: T, memoryVersion?: MemoryVersion) { 27 | super(data, memoryVersion); 28 | } 29 | } 30 | 31 | export function newPersistedValue(data: T): [IPersistedValue, boolean] { 32 | const ret = new PersistedValue(data); 33 | 34 | // @ts-ignore 35 | const tx = PSD.tx as Transaction; 36 | let txComplete = false; 37 | if (tx) { 38 | tx.touch(ret, "create", data); 39 | } else { 40 | txComplete = true; 41 | ret.__transactionComplete("create"); 42 | } 43 | 44 | return [ret, txComplete]; 45 | } 46 | 47 | // nothing to do on hydration. 48 | export function hydratePersistedValue(data: T): IPersistedValue { 49 | // we're reading off disk -- this is thus a value from before any in-flight transaction 50 | // how do we know? 51 | // well there are certain rules the persitence layer must follow to guarantee this. 52 | // TODO: enforce those rules 53 | // We can do so by: 54 | // 1. having a weak ref cache of persisted values 55 | // 2. on hydrate, return the cached value if it exists 56 | // 3. if it does not exist, we're mostly (still not guaranteed yet) safe to return the following statement: 57 | return new PersistedValue(data, memory.MIN_VERSION); 58 | } 59 | -------------------------------------------------------------------------------- /ts/packages/value/src/__tests__/History.test.ts: -------------------------------------------------------------------------------- 1 | import { History } from "../History.js"; 2 | import { inflight, transaction } from "../transaction"; 3 | 4 | test("at", () => { 5 | const history = new History(); 6 | inflight.clear(); 7 | 8 | // empty histories can't return anything 9 | expect(() => history.at(0)).toThrow(); 10 | 11 | history.maybeAdd("first", 1); 12 | 13 | // nothing is in flight so no history should have been added 14 | expect(() => history.at(2)).toThrow(); 15 | 16 | inflight.add(transaction()); 17 | 18 | history.maybeAdd("first", 1); 19 | 20 | // Should get the greatest version less then or equal to the requested version. 21 | expect(history.at(2)).toEqual("first"); 22 | expect(history.at(1)).toEqual("first"); 23 | 24 | history.maybeAdd("second", 2); 25 | history.maybeAdd("third", 3); 26 | 27 | expect(history.at(3)).toEqual("third"); 28 | expect(history.at(2)).toEqual("second"); 29 | expect(history.at(1)).toEqual("first"); 30 | 31 | // going back before any history exists for the node should throw 32 | expect(() => history.at(0)).toThrow(); 33 | }); 34 | 35 | test("maybe add prunes history", () => { 36 | const history = new History(); 37 | inflight.clear(); 38 | 39 | inflight.add(transaction()); 40 | 41 | history.maybeAdd("first", 1); 42 | expect(history.at(1)).toEqual("first"); 43 | 44 | inflight.clear(); 45 | 46 | // history should be cleaned up given no in-flight transactions 47 | history.maybeAdd("first", 1); 48 | expect(() => history.at(1)).toThrow(); 49 | }); 50 | -------------------------------------------------------------------------------- /ts/packages/value/src/__tests__/ObservableValue.test.ts: -------------------------------------------------------------------------------- 1 | import { observableValue } from "../ObservableValue.js"; 2 | import { tx } from "../transaction.js"; 3 | import { createCases } from "./shared-value-tests.js"; 4 | 5 | const tests = createCases((x) => observableValue(x)[0]); 6 | 7 | tests.forEach(([name, fn]) => { 8 | test(name, fn); 9 | }); 10 | 11 | test("Observers are not called back until after tx commit", () => { 12 | const [v] = observableValue(1); 13 | 14 | let notified = false; 15 | v.onTransactionComplete(() => { 16 | notified = true; 17 | }); 18 | 19 | tx(() => { 20 | v.val = 2; 21 | expect(notified).toBe(false); 22 | }); 23 | expect(notified).toBe(true); 24 | }); 25 | -------------------------------------------------------------------------------- /ts/packages/value/src/__tests__/Value.test.ts: -------------------------------------------------------------------------------- 1 | import { value } from "../Value.js"; 2 | import { createCases } from "./shared-value-tests.js"; 3 | 4 | const tests = createCases(value); 5 | 6 | tests.forEach(([name, fn]) => { 7 | test(name, fn); 8 | }); 9 | -------------------------------------------------------------------------------- /ts/packages/value/src/__tests__/demo.test.ts: -------------------------------------------------------------------------------- 1 | import { PSD } from "@vlcn.io/zone"; 2 | import { inflight, tx, txAsync, txSerializedAsync } from "../transaction"; 3 | import { value } from "../Value.js"; 4 | 5 | async function nativePromiseDelay(n: number) { 6 | await new Promise((resolve) => setTimeout(resolve, n)); 7 | } 8 | 9 | function promiseDelay(n: number) { 10 | return new Promise((resolve) => setTimeout(resolve, n)); 11 | } 12 | 13 | test("sibling transactions can be retried until success if they modify the same data concurrently", async () => { 14 | const shared = value(1); 15 | 16 | const task = async () => { 17 | const temp = shared.val; 18 | await promiseDelay(0); 19 | shared.val = temp + 1; 20 | }; 21 | 22 | const t1 = txAsync(task, { concurrentModification: "retry" }); 23 | const t2 = txAsync(task, { concurrentModification: "retry" }); 24 | 25 | // run em concurrently 26 | await Promise.all([t1, t2]); 27 | 28 | // should have retried the loser and incremented to the correct value 29 | expect(shared.val).toBe(3); 30 | console.log("In tx: " + shared.val); 31 | }); 32 | 33 | test("example no tx", async () => { 34 | const shared = value(1); 35 | 36 | const task = async () => { 37 | const temp = shared.val; 38 | await promiseDelay(0); 39 | shared.val = temp + 1; 40 | }; 41 | 42 | // run em concurrently 43 | await Promise.all([task(), task()]); 44 | 45 | console.log("No tx: " + shared.val); 46 | }); 47 | -------------------------------------------------------------------------------- /ts/packages/value/src/__tests__/memory.test.ts: -------------------------------------------------------------------------------- 1 | import { memory } from "../memory.js"; 2 | 3 | test("current version", () => { 4 | // idempotent 5 | expect(memory.version).toBe(memory.version); 6 | 7 | // initialization 8 | expect(memory.version).toBe(Number.MIN_SAFE_INTEGER); 9 | }); 10 | 11 | test("next version", () => { 12 | // next version is persisted 13 | expect(memory.nextVersion()).toBe(memory.version); 14 | 15 | const lastVersion = memory.version; 16 | // monotonically increases 17 | expect(memory.nextVersion()).toBeGreaterThan(lastVersion); 18 | }); 19 | -------------------------------------------------------------------------------- /ts/packages/value/src/index.ts: -------------------------------------------------------------------------------- 1 | export { IValue, value, Event } from "./Value.js"; 2 | export { 3 | tx, 4 | txAsync, 5 | txSerializedAsync, 6 | Transaction, 7 | TxOptions, 8 | } from "./transaction.js"; 9 | export { IObservableValue, observableValue } from "./ObservableValue.js"; 10 | 11 | // Currently unsafe APIs as they require the user to get a number of things right in order to correctly use them 12 | export { 13 | newPersistedValue as newPersistedValue_UNSAFE, 14 | IPersistedValue as IPersistedValue_UNSAFE, 15 | hydratePersistedValue as hydratePersistedValue_UNSAFE, 16 | } from "./PersistedValue.js"; 17 | -------------------------------------------------------------------------------- /ts/packages/value/src/memory.ts: -------------------------------------------------------------------------------- 1 | export type MemoryVersion = number; 2 | let version: MemoryVersion = Number.MIN_SAFE_INTEGER; 3 | 4 | export const memory = Object.freeze({ 5 | MIN_VERSION: Number.MAX_SAFE_INTEGER, 6 | 7 | get version() { 8 | return version; 9 | }, 10 | 11 | nextVersion() { 12 | return ++version; 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /ts/packages/value/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [{ "path": "../zone" }, { "path": "../util" }] 9 | } 10 | -------------------------------------------------------------------------------- /ts/packages/zone/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @vlcn.io/context-provider 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - publish for testing 8 | 9 | ## 0.0.2 10 | 11 | ### Patch Changes 12 | 13 | - retryable transactions, serializable transactions, conflict detection 14 | -------------------------------------------------------------------------------- /ts/packages/zone/README.md: -------------------------------------------------------------------------------- 1 | # context-provider 2 | 3 | Allows storing data in the current execution context and tracks across async calls. 4 | 5 | The `context-provider` package is 100% pulled and isolated from `Dexie.js`. 6 | 7 | ``` 8 | newScope(async () => { 9 | PSD.prop1... 10 | 11 | await foo(); 12 | 13 | PSD.prop1... 14 | }, {prop1: 'value', ...}); 15 | ``` 16 | -------------------------------------------------------------------------------- /ts/packages/zone/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /ts/packages/zone/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vlcn.io/zone", 3 | "version": "0.1.0", 4 | "main": "lib/index.js", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vulcan-sh/vulcan.git", 9 | "directory": "ts/packages/zone" 10 | }, 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "@babel/core": "^7.18.13", 14 | "@babel/preset-env": "^7.18.10", 15 | "@types/jest": "^28.1.8", 16 | "fast-check": "^3.1.2", 17 | "jest": "^29.0.1", 18 | "typescript": "^4.8.2" 19 | }, 20 | "peerDependencies": {}, 21 | "scripts": { 22 | "clean": "tsc --build --clean", 23 | "build": "tsc --build", 24 | "watch": "tsc --build -w", 25 | "test": "node ./node_modules/jest/bin/jest.js", 26 | "deep-clean": "rm -rf ./lib || true && rm tsconfig.tsbuildinfo || true" 27 | }, 28 | "jest": { 29 | "testMatch": [ 30 | "**/__tests__/**/*.test.js" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ts/packages/zone/src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { newScope, PSD } from "../index.js"; 2 | 3 | test("async/await", async () => { 4 | let c = 0; 5 | const task = async () => { 6 | let prop = ++c; 7 | const ctx = { 8 | prop, 9 | }; 10 | await newScope(async () => { 11 | // TODO: fix up dexie types to understand psd extension 12 | // @ts-ignore 13 | expect(prop).toBe(PSD.prop); 14 | 15 | await new Promise((resolve) => setTimeout(resolve, Math.random() * 50)); 16 | // @ts-ignore 17 | expect(prop).toBe(PSD.prop); 18 | }, ctx); 19 | }; 20 | 21 | await Promise.all([task(), task(), task(), task(), task()]); 22 | }); 23 | 24 | test("promise", () => { 25 | let c = 0; 26 | const task = () => { 27 | let prop = ++c; 28 | const ctx = { 29 | prop, 30 | }; 31 | return newScope(() => { 32 | // @ts-ignore 33 | expect(prop).toBe(PSD.prop); 34 | 35 | new Promise((resolve) => setTimeout(resolve, Math.random() * 50)).then( 36 | () => { 37 | // @ts-ignore 38 | expect(prop).toBe(PSD.prop); 39 | } 40 | ); 41 | }, ctx); 42 | }; 43 | 44 | return Promise.all([task(), task(), task(), task(), task()]); 45 | }); 46 | 47 | // dexie doesn't patch setTimeout or other APIs but that's fine -- users shouldn't be spreading a tx out over time via anything other 48 | // than promises and async/await. 49 | // If we do run into a valid use case... we'll figure out how we should patch the required APIs then. 50 | // Note: could we fix OpenTelemetry based on the Dexie approach? 51 | // - https://github.com/aphrodite-sh/acid-memory/commit/fef0d84765dadd26920fc91bb1ac3ed443c59840 52 | -------------------------------------------------------------------------------- /ts/packages/zone/src/dexie/README.md: -------------------------------------------------------------------------------- 1 | Set of files lifted from `Dexie.js` to enable async context propagation. 2 | 3 | Initially tried `zone.js` but that does not work with native `async & await`. See commit: https://github.com/aphrodite-sh/acid-memory/commit/fef0d84765dadd26920fc91bb1ac3ed443c59840 4 | -------------------------------------------------------------------------------- /ts/packages/zone/src/dexie/globals/global.ts: -------------------------------------------------------------------------------- 1 | declare var global: any; 2 | export const _global: any = 3 | typeof globalThis !== "undefined" 4 | ? globalThis 5 | : typeof self !== "undefined" 6 | ? self 7 | : typeof window !== "undefined" 8 | ? window 9 | : global; 10 | -------------------------------------------------------------------------------- /ts/packages/zone/src/dexie/helpers/debug.ts: -------------------------------------------------------------------------------- 1 | // Lifted from `dexie.js` -- all the @ts-ignore's are because dexie doesn't use ts-strict and has type errors. 2 | 3 | // By default, debug will be true only if platform is a web platform and its page is served from localhost. 4 | // When debug = true, error's stacks will contain asyncronic long stacks. 5 | export var debug = 6 | typeof location !== "undefined" && 7 | // By default, use debug mode if served from localhost. 8 | /^(http|https):\/\/(localhost|127\.0\.0\.1)/.test(location.href); 9 | 10 | // @ts-ignore 11 | export function setDebug(value, filter) { 12 | debug = value; 13 | libraryFilter = filter; 14 | } 15 | 16 | export var libraryFilter = () => true; 17 | 18 | export const NEEDS_THROW_FOR_STACK = !new Error("").stack; 19 | 20 | export function getErrorWithStack() { 21 | "use strict"; 22 | if (NEEDS_THROW_FOR_STACK) 23 | try { 24 | // Doing something naughty in strict mode here to trigger a specific error 25 | // that can be explicitely ignored in debugger's exception settings. 26 | // If we'd just throw new Error() here, IE's debugger's exception settings 27 | // will just consider it as "exception thrown by javascript code" which is 28 | // something you wouldn't want it to ignore. 29 | getErrorWithStack.arguments; 30 | throw new Error(); // Fallback if above line don't throw. 31 | } catch (e) { 32 | return e; 33 | } 34 | return new Error(); 35 | } 36 | 37 | // @ts-ignore 38 | export function prettyStack(exception, numIgnoredFrames) { 39 | var stack = exception.stack; 40 | if (!stack) return ""; 41 | numIgnoredFrames = numIgnoredFrames || 0; 42 | if (stack.indexOf(exception.name) === 0) 43 | numIgnoredFrames += (exception.name + exception.message).split("\n").length; 44 | return ( 45 | stack 46 | .split("\n") 47 | .slice(numIgnoredFrames) 48 | .filter(libraryFilter) 49 | // @ts-ignore 50 | .map((frame) => "\n" + frame) 51 | .join("") 52 | ); 53 | } 54 | 55 | // TODO: Replace this in favor of a decorator instead. 56 | // @ts-ignore 57 | export function deprecated(what: string, fn: (...args) => T) { 58 | return function () { 59 | console.warn( 60 | `${what} is deprecated. See https://dexie.org/docs/Deprecations. ${prettyStack( 61 | getErrorWithStack(), 62 | 1 63 | )}` 64 | ); 65 | // @ts-ignore 66 | return fn.apply(this, arguments); 67 | } as (...args: any[]) => T; 68 | } 69 | -------------------------------------------------------------------------------- /ts/packages/zone/src/index.ts: -------------------------------------------------------------------------------- 1 | export { isAsyncFunction } from "./dexie/functions/utils.js"; 2 | 3 | export { 4 | newScope, 5 | PSD, 6 | incrementExpectedAwaits, 7 | decrementExpectedAwaits, 8 | DexiePromise as ZonedPromise, 9 | } from "./dexie/helpers/promise.js"; 10 | -------------------------------------------------------------------------------- /ts/packages/zone/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-lib.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/"], 8 | "references": [] 9 | } 10 | -------------------------------------------------------------------------------- /ts/tsconfig-lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "strictNullChecks": true, 5 | "module": "esnext", 6 | "target": "esnext", 7 | "skipLibCheck": true, 8 | "moduleResolution": "Node", 9 | "baseUrl": "./src/", 10 | "declaration": true, 11 | "allowJs": true, 12 | "composite": true, 13 | "declarationMap": true, 14 | "incremental": true, 15 | "strict": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ts/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseBranch": "origin/main", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": [], 6 | "outputs": [] 7 | }, 8 | "test": { 9 | "dependsOn": [] 10 | }, 11 | "clean": { 12 | "dependsOn": [] 13 | }, 14 | "deep-clean": { 15 | "dependsOn": [] 16 | } 17 | } 18 | } 19 | --------------------------------------------------------------------------------