Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
"@effect/docgen": "^0.4.4",
"@effect/dtslint": "^0.1.1",
"@effect/eslint-plugin": "^0.3.2",
"@effect/language-service": "^0.23.5",
"@effect/vitest": "^0.29.0",
"@effect/language-service": "^0.86.2",
"@effect/vitest": "4.0.0-beta.78",
"@eslint/compat": "1.4.1",
"@eslint/eslintrc": "3.3.5",
"@eslint/js": "9.39.4",
Expand Down
12 changes: 4 additions & 8 deletions packages/cypher-codegen/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,15 @@
"antlr4ng": "^3.0.16"
},
"peerDependencies": {
"@effect/cli": "^0.75.0",
"@effect/platform": "^0.96.0",
"@effect/platform-node": "^0.106.0",
"effect": "^3.21.0",
"@effect/platform-node": "^4.0.0-beta.78",
"effect": "^4.0.0-beta.78",
"neo4j-driver": "^6.0.1"
},
"devDependencies": {
"@effect/cli": "^0.75.0",
"@effect/platform": "^0.96.0",
"@effect/platform-node": "^0.106.0",
"@effect/platform-node": "4.0.0-beta.78",
"@evryg/effect-vitest-neo4j": "workspace:^",
"antlr-ng": "^1.0.10",
"effect": "^3.21.0",
"effect": "4.0.0-beta.78",
"neo4j-driver": "^6.0.1"
}
}
13 changes: 7 additions & 6 deletions packages/cypher-codegen/src/Codegen.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/** @since 0.0.1 */
import { Command } from "@effect/cli"
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { NodeRuntime, NodeServices } from "@effect/platform-node"
import { Effect } from "effect"
import type { Schema } from "effect"
import { Command } from "effect/unstable/cli"
import { makeApplySchemaCommand } from "./internal/cli/commands/ApplySchema.js"
import { extractSchemaCommand } from "./internal/cli/commands/ExtractSchema.js"
import { makeGenerateCommand } from "./internal/cli/commands/Generate.js"
Expand All @@ -11,12 +11,13 @@ import { makeGenerateCommand } from "./internal/cli/commands/Generate.js"
* @since 0.0.1
* @category cli
*/
export function runCodegenCli(allSchemas: Array<Schema.Schema.Any>): void {
export function runCodegenCli(allSchemas: Array<Schema.Top>): void {
const rootCommand = Command.make("cypher-codegen").pipe(
Command.withSubcommands([extractSchemaCommand, makeGenerateCommand(allSchemas), makeApplySchemaCommand(allSchemas)])
)

const cli = Command.run(rootCommand, { name: "cypher-codegen", version: "0.1.0" })

cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain)
Command.run(rootCommand, { version: "0.1.0" }).pipe(
Effect.provide(NodeServices.layer),
NodeRuntime.runMain
)
}
4 changes: 2 additions & 2 deletions packages/cypher-codegen/src/Register.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @since 0.0.1 */
import { NodeContext } from "@effect/platform-node"
import { NodeServices } from "@effect/platform-node"
import { type GraphSchema, loadSchema } from "@evryg/effect-neo4j-schema"
import { Effect } from "effect"
import { readFileSync } from "node:fs"
Expand All @@ -10,7 +10,7 @@ import { analyzeQuery } from "./frontend/QueryAnalyzer.js"

let schema: GraphSchema | undefined
try {
schema = Effect.runSync(loadSchema("data/graph-schema.json").pipe(Effect.provide(NodeContext.layer)))
schema = Effect.runSync(loadSchema("data/graph-schema.json").pipe(Effect.provide(NodeServices.layer)))
} catch {
// Schema not available — fall back to untyped codegen
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,11 @@ describe("generateModule with columns (typed codegen)", () => {
expect(source).toContain(".toObject()")
})

it("composes Neo4jRecordToObject with Row via Schema.compose", () => {
it("composes Neo4jRecordToObject with Row via decodeTo", () => {
const source = generateModule("MATCH (c:Class) RETURN c.fqcn AS fqcn", [
col("fqcn", S("String"), false)
])
expect(source).toContain("Schema.Array(Schema.compose(Neo4jRecordToObject, Row, { strict: false }))")
expect(source).toContain("Schema.Array(Neo4jRecordToObject.pipe(Schema.decodeTo(Row)))")
})

it("passes decoder directly to Effect.map (no lambda)", () => {
Expand Down Expand Up @@ -139,7 +139,7 @@ describe("generateModule with columns (typed codegen)", () => {
])
expect(source).toContain("{ fqcn }")
expect(source).toContain("Schema.Struct")
expect(source).toContain("Schema.compose(Neo4jRecordToObject, Row")
expect(source).toContain("Neo4jRecordToObject.pipe(Schema.decodeTo(Row")
})

it("emits Neo4jValue for UnknownType columns (escape hatch)", () => {
Expand Down Expand Up @@ -255,20 +255,20 @@ describe("generateBarrel — typed params", () => {
params: []
}
const source = generateBarrel([entry])
const matches = source.match(/Neo4jRecordToObject = Schema\.transform/g)
const matches = source.match(/Neo4jRecordToObject = Schema\.Unknown\.pipe\(Schema\.decodeTo/g)
expect(matches).toHaveLength(1)
expect(source).toContain(".toObject()")
})

it("composes Neo4jRecordToObject with row schema via Schema.compose", () => {
it("composes Neo4jRecordToObject with row schema via decodeTo", () => {
const entry: BarrelEntry = {
filename: "Foo.cypher",
cypher: "MATCH (c:Class) RETURN c.fqcn AS fqcn",
columns: [col("fqcn", S("String"), false)],
params: []
}
const source = generateBarrel([entry])
expect(source).toContain("Schema.Array(Schema.compose(Neo4jRecordToObject, fooQueryRow, { strict: false }))")
expect(source).toContain("Schema.Array(Neo4jRecordToObject.pipe(Schema.decodeTo(fooQueryRow)))")
})

it("passes decoder directly to Effect.map (no lambda)", () => {
Expand Down
45 changes: 24 additions & 21 deletions packages/cypher-codegen/src/backend/CypherCodegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ function generateTypedModule(cypher: string, columns: ReadonlyArray<ResolvedColu
const lines: Array<string> = []

// Imports
lines.push(`import { Effect, Schema } from "effect";`)
lines.push(`import { Effect, Schema, SchemaTransformation } from "effect";`)
const neo4jImports = neo4jSchemaImports(columns)
if (neo4jImports.length > 0) {
lines.push(`import { Neo4jClient, ${neo4jImports.join(", ")} } from "@evryg/effect-neo4j";`)
Expand All @@ -197,20 +197,20 @@ function generateTypedModule(cypher: string, columns: ReadonlyArray<ResolvedColu
lines.push(``)

// Neo4j Record → plain object transform
lines.push(`const Neo4jRecordToObject = Schema.transform(`)
lines.push(` Schema.Unknown, Schema.Unknown,`)
lines.push(`const Neo4jRecordToObject = Schema.Unknown.pipe(Schema.decodeTo(`)
lines.push(` Schema.Unknown,`)
lines.push(
` { strict: true, decode: (rec) => (rec as { toObject(): Record<string, unknown> }).toObject(), encode: (obj) => obj },`
` SchemaTransformation.transform({ decode: (rec) => (rec as { toObject(): Record<string, unknown> }).toObject(), encode: (obj) => obj }),`
)
lines.push(`);`)
lines.push(`));`)
lines.push(``)

// Temporal string transform (only if needed)
if (needsTemporalString(columns)) {
lines.push(`const TemporalString = Schema.transform(`)
lines.push(` Schema.Unknown, Schema.String,`)
lines.push(` { decode: (v) => (v).toString(), encode: (s) => s },`)
lines.push(`);`)
lines.push(`const TemporalString = Schema.Unknown.pipe(Schema.decodeTo(`)
lines.push(` Schema.String,`)
lines.push(` SchemaTransformation.transform({ decode: (v) => (v).toString(), encode: (s) => s }),`)
lines.push(`));`)
lines.push(``)
}

Expand All @@ -224,7 +224,7 @@ function generateTypedModule(cypher: string, columns: ReadonlyArray<ResolvedColu

// Decoder (compose transform + struct, wrap in array)
lines.push(`const decodeRows = Schema.decodeUnknownSync(`)
lines.push(` Schema.Array(Schema.compose(Neo4jRecordToObject, Row, { strict: false }))`)
lines.push(` Schema.Array(Neo4jRecordToObject.pipe(Schema.decodeTo(Row)))`)
lines.push(`);`)
lines.push(``)

Expand Down Expand Up @@ -273,8 +273,10 @@ export interface BarrelEntry {
export function generateBarrel(entries: ReadonlyArray<BarrelEntry>): string {
const lines: Array<string> = []

const anyHasColumns = entries.some((e) => e.columns.length > 0)

lines.push(`// Auto-generated by cypher-codegen — do not edit`)
lines.push(`import { Effect, Schema } from "effect"`)
lines.push(`import { Effect, Schema${anyHasColumns ? ", SchemaTransformation" : ""} } from "effect"`)

// Collect all neo4j schema imports needed across all entries
const allColumns = entries.flatMap((e) => e.columns)
Expand All @@ -287,24 +289,25 @@ export function generateBarrel(entries: ReadonlyArray<BarrelEntry>): string {
lines.push(``)

// Shared transforms (emit once)
const anyHasColumns = entries.some((e) => e.columns.length > 0)
if (anyHasColumns) {
lines.push(`const Neo4jRecordToObject = Schema.transform(`)
lines.push(` Schema.Unknown, Schema.Unknown,`)
lines.push(`const Neo4jRecordToObject = Schema.Unknown.pipe(Schema.decodeTo(`)
lines.push(` Schema.Unknown,`)
lines.push(
` { strict: true, decode: (rec) => (rec as { toObject(): Record<string, unknown> }).toObject(), encode: (obj) => obj },`
` SchemaTransformation.transform({ decode: (rec) => (rec as { toObject(): Record<string, unknown> }).toObject(), encode: (obj) => obj }),`
)
lines.push(`)`)
lines.push(`))`)
lines.push(``)
}

const anyNeedTemporal = entries.some((e) => needsTemporalString(e.columns))

if (anyNeedTemporal) {
lines.push(`const TemporalString = Schema.transform(`)
lines.push(` Schema.Unknown, Schema.String,`)
lines.push(` { decode: (v) => (v as { toString(): string }).toString(), encode: (s: string) => s },`)
lines.push(`)`)
lines.push(`const TemporalString = Schema.Unknown.pipe(Schema.decodeTo(`)
lines.push(` Schema.String,`)
lines.push(
` SchemaTransformation.transform({ decode: (v) => (v as { toString(): string }).toString(), encode: (s: string) => s }),`
)
lines.push(`))`)
lines.push(``)
}

Expand Down Expand Up @@ -336,7 +339,7 @@ export function generateBarrel(entries: ReadonlyArray<BarrelEntry>): string {
lines.push(`export type ${name.charAt(0).toUpperCase() + name.slice(1)}Row = typeof ${name}Row.Type`)
lines.push(``)
lines.push(`const ${decodeName} = Schema.decodeUnknownSync(`)
lines.push(` Schema.Array(Schema.compose(Neo4jRecordToObject, ${name}Row, { strict: false }))`)
lines.push(` Schema.Array(Neo4jRecordToObject.pipe(Schema.decodeTo(${name}Row)))`)
lines.push(`)`)
lines.push(``)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ CREATE (prop)-[:BELONGS_TO {role: "class"}]->(c2)
`

layer(TestNeo4j, { timeout: "120 seconds" })("QueryAnalyzer — schema extraction + inference (integration)", (it) => {
it.scoped("extracts schema with all expected labels and rel types", () =>
it.effect("extracts schema with all expected labels and rel types", () =>
Effect.gen(function*() {
yield* CleanNeo4jGraph
const client = yield* Neo4jClient
Expand Down Expand Up @@ -143,7 +143,7 @@ layer(TestNeo4j, { timeout: "120 seconds" })("QueryAnalyzer — schema extractio
}
}))

it.scoped("analyzeQuery produces no UnknownType for representative .cypher files", () =>
it.effect("analyzeQuery produces no UnknownType for representative .cypher files", () =>
Effect.gen(function*() {
yield* CleanNeo4jGraph
const client = yield* Neo4jClient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { query as fixtureQuery } from "./Fixture.cypher"
const TestNeo4j = UnconfiguredNeo4jClient.pipe(Layer.provide(Neo4jConfigFromVitest))

layer(TestNeo4j, { timeout: "120 seconds" })("Cypher plugin (integration)", (it) => {
it.scoped("Fixture.cypher query runs against real Neo4j and returns empty result", () =>
it.effect("Fixture.cypher query runs against real Neo4j and returns empty result", () =>
Effect.gen(function*() {
yield* CleanNeo4jGraph
const records = yield* fixtureQuery()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
/** @since 0.0.1 */
import { Command } from "@effect/cli"
import { Neo4jClient } from "@evryg/effect-neo4j"
import { compileToCypherDDL } from "@evryg/effect-neo4j-schema"
import { Console, Effect } from "effect"
import type { Schema } from "effect"
import { Command } from "effect/unstable/cli"
import { neo4jLayer, neo4jOptions } from "./Shared.js"

/**
* @since 0.0.1
* @category cli
*/
export const makeApplySchemaCommand = (allSchemas: Array<Schema.Schema.Any>) =>
export const makeApplySchemaCommand = (allSchemas: Array<Schema.Top>) =>
Command.make(
"apply-schema",
{ ...neo4jOptions },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/** @since 0.0.1 */
import { Command } from "@effect/cli"
import { NodeContext } from "@effect/platform-node"
import { NodeServices } from "@effect/platform-node"
import { extractSchema, saveSchema } from "@evryg/effect-neo4j-schema"
import { Console, Effect } from "effect"
import { Command } from "effect/unstable/cli"
import { neo4jLayer, neo4jOptions, schemaPathOption } from "./Shared.js"

/**
Expand All @@ -20,5 +20,5 @@ export const extractSchemaCommand = Command.make(
yield* Console.log(
` ${schema.vertexProperties.length} vertex properties, ${schema.edgeProperties.length} edge properties`
)
}).pipe(Effect.provide([neo4jLayer(opts), NodeContext.layer]))
}).pipe(Effect.provide([neo4jLayer(opts), NodeServices.layer]))
)
10 changes: 5 additions & 5 deletions packages/cypher-codegen/src/internal/cli/commands/Generate.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/** @since 0.0.1 */
import { Command } from "@effect/cli"
import { NodeContext } from "@effect/platform-node"
import { NodeServices } from "@effect/platform-node"
import { compileToGraphSchema, extractSchema, saveSchema } from "@evryg/effect-neo4j-schema"
import { Console, Effect } from "effect"
import type { Schema } from "effect"
import { Command } from "effect/unstable/cli"
import {
cypherGlobOption,
generateFromSchema,
Expand All @@ -24,12 +24,12 @@ const generateLiveDbCommand = Command.make(
yield* saveSchema(opts.schemaPath, schema)
yield* Console.log(`Schema extracted: ${schema.vertexProperties.length} vertex properties`)
yield* generateFromSchema(schema, opts.output, opts.cypherGlob)
}).pipe(Effect.provide([neo4jLayer(opts), NodeContext.layer]))
}).pipe(Effect.provide([neo4jLayer(opts), NodeServices.layer]))
)

// ── generate annotations ──

const makeGenerateAnnotationsCommand = (allSchemas: Array<Schema.Schema.Any>) =>
const makeGenerateAnnotationsCommand = (allSchemas: Array<Schema.Top>) =>
Command.make(
"annotations",
{ output: outputOption, cypherGlob: cypherGlobOption },
Expand All @@ -49,7 +49,7 @@ const makeGenerateAnnotationsCommand = (allSchemas: Array<Schema.Schema.Any>) =>
* @since 0.0.1
* @category cli
*/
export const makeGenerateCommand = (allSchemas: Array<Schema.Schema.Any>) =>
export const makeGenerateCommand = (allSchemas: Array<Schema.Top>) =>
Command.make("generate").pipe(
Command.withSubcommands([generateLiveDbCommand, makeGenerateAnnotationsCommand(allSchemas)])
)
Loading
Loading