Port Compile API extensions from EOPA (#7887)

* server: port compile API

Also adds e2e tests: These include coverage for ucast in the prisma
setting, and thus require some JS runtime.

* e2e: selectively skip e2e Compile API tests

...for macos runs, and for the go-compat suites.

* server: accept timer_rego_external_resolve_ns metrics with value 0

When running the tests in a loop for a while, I would see values of 0ns
for this metric. However, comparing with its non-zero values, which are
often 41 or 42ns, it seems like this is just not happening in this code
path. So if "almost nothing" actually goes below 1ns, it's OK.

* e2e: split dep-heavy e2e tests into their own go module
* Makefile: export DOCKER_RUNNING (make e2e read it)

---------

Co-authored-by: Philip Conrad <philip@chariot-chaser.net>
Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
This commit is contained in:
Stephan Renatus
2025-09-22 12:11:06 +02:00
committed by GitHub
parent 5816e4f4f8
commit 4ab2ac0c61
243 changed files with 33516 additions and 4256 deletions
+794
View File
@@ -0,0 +1,794 @@
// Copyright 2025 The OPA Authors
// SPDX-License-Identifier: Apache-2.0
package compile
import (
"bufio"
"bytes"
"context"
"database/sql"
_ "embed"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"slices"
"strings"
"testing"
"time"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
_ "github.com/microsoft/go-mssqldb"
_ "modernc.org/sqlite"
"github.com/docker/go-connections/nat"
"github.com/google/go-cmp/cmp"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"github.com/open-policy-agent/opa/v1/test/e2e"
)
type DBType string
const (
Postgres DBType = "postgresql"
MySQL DBType = "mysql"
MSSQL DBType = "sqlserver"
SQLite DBType = "sqlite"
)
// TestConfig holds test configuration
type TestConfig struct {
db *sql.DB
dbName string
dbType DBType
dbURL string
}
// containerConfig holds database-specific container configuration
type containerConfig struct {
image string
port string
env map[string]string
waitFor wait.Strategy
urlTemplate string
}
var dbConfigs = map[DBType]containerConfig{
Postgres: {
image: "postgres:17-alpine",
port: "5432/tcp",
env: map[string]string{
"POSTGRES_DB": "testdb",
"POSTGRES_USER": "testuser",
"POSTGRES_PASSWORD": "testpass",
},
waitFor: wait.ForLog("database system is ready to accept connections").
WithOccurrence(2).
WithStartupTimeout(15 * time.Second),
urlTemplate: "postgres://testuser:testpass@%s:%s/testdb?sslmode=disable",
},
MySQL: {
image: "mysql:9",
port: "3306/tcp",
env: map[string]string{
"MYSQL_DATABASE": "testdb",
"MYSQL_USER": "testuser",
"MYSQL_PASSWORD": "testpass",
"MYSQL_ROOT_PASSWORD": "rootpass",
},
waitFor: wait.ForLog("port: 3306 MySQL Community Server"),
urlTemplate: "testuser:testpass@tcp(%s:%s)/testdb",
},
MSSQL: {
image: "mcr.microsoft.com/mssql/server:2022-latest",
port: "1433/tcp",
env: map[string]string{
"ACCEPT_EULA": "Y",
"MSSQL_SA_PASSWORD": "MyStr0ngPassw0rd!",
},
waitFor: wait.ForLog("Recovery is complete."),
urlTemplate: "sqlserver://sa:MyStr0ngPassw0rd!@%s:%s",
},
SQLite: {
urlTemplate: ":memory:",
},
}
// setupTestContainer creates and starts a database container
func setupTestContainer(ctx context.Context, dbType DBType) (testcontainers.Container, string, error) {
if dbType == SQLite {
return nil, ":memory:", nil
}
config := dbConfigs[dbType]
containerReq := testcontainers.ContainerRequest{
Image: config.image,
ExposedPorts: []string{config.port},
WaitingFor: config.waitFor,
Env: config.env,
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: containerReq,
Started: true,
})
if err != nil {
return nil, "", fmt.Errorf("failed to start container: %v", err)
}
port, err := container.MappedPort(ctx, nat.Port(config.port))
if err != nil {
return nil, "", fmt.Errorf("failed to get container port: %v", err)
}
host, err := container.Host(ctx)
if err != nil {
return nil, "", fmt.Errorf("failed to get container host: %v", err)
}
dbURL := fmt.Sprintf(config.urlTemplate, host, port.Port())
return container, dbURL, nil
}
// getCreateTableSQL returns database-specific CREATE TABLE SQL
func getCreateTableSQL(dbType DBType) string {
switch dbType {
case Postgres:
return `
CREATE TABLE IF NOT EXISTS fruit (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
colour VARCHAR(100) NOT NULL,
price INT
)`
case MySQL:
return `
CREATE TABLE IF NOT EXISTS fruit (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
colour VARCHAR(100) NOT NULL,
price INT
)`
case SQLite:
return `
CREATE TABLE IF NOT EXISTS fruit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL,
colour VARCHAR(100) NOT NULL,
price INT
)`
case MSSQL:
return `
IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='fruit' AND xtype='U')
CREATE TABLE fruit (
id INT IDENTITY(1,1) PRIMARY KEY,
name NVARCHAR(100) NOT NULL,
colour NVARCHAR(100) NOT NULL,
price INT
)`
}
panic("unknown db type")
}
// initializeTestData sets up initial test data in the database
func initializeTestData(db *sql.DB, dbType DBType) error {
createTableSQL := getCreateTableSQL(dbType)
if _, err := db.Exec(createTableSQL); err != nil {
return fmt.Errorf("failed to create table: %v", err)
}
// Insert test data - using parameterized queries for better compatibility
var insertDataSQL string
switch dbType {
case Postgres:
insertDataSQL = "INSERT INTO fruit (name, colour, price) VALUES ($1, $2, $3)"
case MSSQL:
insertDataSQL = "INSERT INTO fruit (name, colour, price) VALUES (@p1, @p2, @p3)"
default:
insertDataSQL = "INSERT INTO fruit (name, colour, price) VALUES (?, ?, ?)"
}
for _, f := range []struct {
name string
colour string
price int
}{
{"apple", "green", 10},
{"banana", "yellow", 20},
{"cherry", "red", 11},
} {
if _, err := db.Exec(insertDataSQL, f.name, f.colour, f.price); err != nil {
return fmt.Errorf("failed to insert test data: %v", err)
}
}
if _, err := db.Exec(insertDataSQL, "orange", "orange", nil); err != nil {
return fmt.Errorf("failed to insert test data: %v", err)
}
return nil
}
// setupDB initializes a test database of the specified type
func setupDB(t *testing.T, dbType DBType) (*TestConfig, func()) {
t.Helper()
ctx := context.Background()
container, dbURL, err := setupTestContainer(ctx, dbType)
if err != nil {
t.Fatalf("failed to setup test container: %v", err)
}
typ := string(dbType)
if dbType == Postgres {
typ = "postgres"
}
db, err := sql.Open(typ, dbURL)
if err != nil {
t.Fatalf("failed to connect to database: %v", err)
}
if dbType == SQLite {
db.SetMaxOpenConns(1)
}
if err := initializeTestData(db, dbType); err != nil {
t.Fatalf("failed to initialize test data: %v", err)
}
cleanup := func() {
if container == nil {
return
}
if err := db.Close(); err != nil {
t.Errorf("failed to close database connection: %v", err)
}
if err := container.Terminate(ctx); err != nil {
t.Errorf("failed to terminate container: %v", err)
}
}
return &TestConfig{
db: db,
dbName: "testdb",
dbType: dbType,
dbURL: dbURL,
}, cleanup
}
type fruitRow struct {
ID int
Name string
Colour string
Price sql.NullInt64
}
type fruitJSON struct {
ID int
Name string
Colour string
Price *int
}
func toFruitRows(xs []fruitJSON) []fruitRow {
rows := make([]fruitRow, len(xs))
for i, x := range xs {
rows[i] = fruitRow{
ID: x.ID,
Name: x.Name,
Colour: x.Colour,
}
if x.Price != nil {
rows[i].Price = sql.NullInt64{Int64: int64(*x.Price), Valid: true}
}
}
return rows
}
// In these test, we test the compile API end-to-end. We start an instance of
// OPA, load a policy, and then run a series of tests that compile a query and
// then execute it against a database. The query is a simple "include" query
// that filters rows from a table based on some conditions. The conditions are
// defined in the data policy.
func TestCompileHappyPathE2E(t *testing.T) {
// NOTE(sr): Technically, we don't need a "docker" binary for these tests to use
// docker via testcontainers. But being able to run "docker ps" is an acceptable litmus
// test. By relying on the Makefile checking this for us, we don't have to adjust all
// the test runs (basic linux/windows, go-compat, race-detector).
if os.Getenv("DOCKER_RUNNING") != "1" {
t.Skip("docker-dependant tests are not runnable")
}
ctx, cancel := context.WithCancel(t.Context())
params := e2e.NewAPIServerTestParams()
params.Addrs = &[]string{"0.0.0.0:0"}
testRuntime, err := e2e.NewTestRuntime(params)
if err != nil {
t.Fatal(err)
}
done := make(chan bool)
go func() {
err := testRuntime.Runtime.Serve(ctx)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
done <- true
}()
t.Cleanup(cancel)
if err := testRuntime.WaitForServer(); err != nil {
t.Fatal(err)
}
opaURL := testRuntime.URL()
t.Log(opaURL)
dbTypes := []DBType{Postgres, MySQL, MSSQL, SQLite}
unknowns := []string{"input.fruits"}
var input any = map[string]any{"fav_colour": "yellow"}
path := `filters%d/include`
mapping := map[string]any{
"fruits": map[string]any{
"$self": "fruit",
},
}
price := func(x int) sql.NullInt64 { return sql.NullInt64{Int64: int64(x), Valid: true} }
apple := fruitRow{ID: 1, Name: "apple", Colour: "green", Price: price(10)}
banana := fruitRow{ID: 2, Name: "banana", Colour: "yellow", Price: price(20)}
cherry := fruitRow{ID: 3, Name: "cherry", Colour: "red", Price: price(11)}
orange := fruitRow{ID: 4, Name: "orange", Colour: "orange"}
tests := []struct {
name string
policy string
expRows []fruitRow
prisma bool
exclude []DBType
}{
{
name: "no conditions",
policy: `include if true`,
expRows: []fruitRow{apple, banana, cherry, orange},
prisma: true,
},
{
name: "unconditional NO",
policy: `include if false`,
prisma: true,
},
{
name: "simple equality",
policy: `include if input.fruits.colour == input.fav_colour`,
expRows: []fruitRow{banana},
prisma: true,
},
{
name: "comparison with two unknowns",
policy: `include if input.fruits.price >= input.fruits.id`,
expRows: []fruitRow{apple, banana, cherry},
},
{
name: "simple comparison",
policy: `include if input.fruits.price < 11`,
expRows: []fruitRow{apple},
prisma: true,
},
{
name: "equal null",
policy: `include if input.fruits.price == null`,
expRows: []fruitRow{orange},
prisma: true,
},
{
name: "not equal null",
policy: `include if input.fruits.price != null`,
expRows: []fruitRow{apple, banana, cherry},
prisma: true,
},
{
name: "simple startswith",
policy: `include if startswith(input.fruits.name, "app")`,
expRows: []fruitRow{apple},
prisma: true,
exclude: []DBType{SQLite},
},
{
name: "simple contains",
policy: `include if contains(input.fruits.name, "a")`,
expRows: []fruitRow{apple, banana, orange},
prisma: true,
exclude: []DBType{SQLite},
},
{
name: "startswith + escaping '_'",
policy: `include if startswith(input.fruits.name, "ap_")`, // if "_" wasn't escaped properly, it would match "apple"
expRows: nil,
exclude: []DBType{SQLite},
},
{
name: "startswith + escaping '%'",
policy: `include if startswith(input.fruits.name, "%ppl")`, // if "%" wasn't escaped properly, it would match "apple"
expRows: nil,
exclude: []DBType{SQLite},
},
{
name: "simple endswith",
policy: `include if endswith(input.fruits.name, "le")`,
expRows: []fruitRow{apple},
prisma: true,
exclude: []DBType{SQLite},
},
{
name: "internal.member_2",
policy: `include if input.fruits.name in {"apple", "cherry", "pineapple"}`,
expRows: []fruitRow{apple, cherry},
prisma: true,
},
{
name: "conjunct query, inequality",
policy: `include if {
input.fruits.name != "apple"
input.fruits.name != "banana"
}`,
expRows: []fruitRow{cherry, orange},
prisma: true,
},
{
name: "disjunct query, equality",
policy: `include if input.fruits.name == "apple"
include if input.fruits.name == "banana"`,
expRows: []fruitRow{apple, banana},
prisma: true,
},
{
name: "not+internal.member_2",
policy: `include if not input.fruits.name in {"apple", "cherry", "pineapple", "orange"}`,
expRows: []fruitRow{banana},
prisma: true,
},
{
name: "not+lt",
policy: `include if not input.fruits.price < 12`,
expRows: []fruitRow{banana},
prisma: true,
},
}
for _, dbType := range dbTypes {
t.Run(string(dbType), func(t *testing.T) {
t.Parallel()
config, cleanup := setupDB(t, dbType)
t.Cleanup(cleanup)
for i, tt := range tests {
// first, we override the policy with the current test case
policy := fmt.Sprintf("package filters%d\n%s", i, tt.policy)
req, err := http.NewRequest("PUT", fmt.Sprintf("%s/v1/policies/policy%d.rego", opaURL, i), strings.NewReader(policy))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
if _, err := http.DefaultClient.Do(req); err != nil {
t.Fatalf("post policy: %v", err)
}
path := fmt.Sprintf(path, i)
if tt.prisma && dbType == Postgres {
t.Run("prisma/"+tt.name, func(t *testing.T) { // also run via our prisma contraption
t.Parallel()
// second, query the compile API
payload := map[string]any{
"input": input,
"unknowns": unknowns,
"options": map[string]any{
"targetSQLTableMappings": map[string]any{
"ucast": mapping,
},
},
}
// get the UCAST IR, process with @styra/ucast-prisma, and run a findMany query
// with that against the DB, return rows
rowsData := getUCASTAndRunPrisma(t, path, payload, config, opaURL)
if diff := cmp.Diff(tt.expRows, rowsData); diff != "" {
t.Errorf("unexpected result (-want +got):\n%s", diff)
}
})
}
t.Run("db/"+tt.name, func(t *testing.T) {
t.Parallel()
if slices.Contains(tt.exclude, dbType) {
t.Skip("skipped via exclude")
}
// second, query the compile API
mappings := make(map[string]any, 1)
mappings[string(config.dbType)] = mapping
payload := map[string]any{
"input": input,
"unknowns": unknowns,
"options": map[string]any{
"targetSQLTableMappings": mappings,
},
}
// get the SQL where clauses, and run the concatenated query against db,
// return rows
rowsData := getSQLAndRunQuery(t, path, payload, config, opaURL)
// finally, compare with expected!
if diff := cmp.Diff(tt.expRows, rowsData); diff != "" {
t.Errorf("unexpected result (-want +got):\n%s", diff)
}
})
}
})
}
}
// This test runs three Compile API queries and asserts that the handler
// execution added something to the exposed prometheus metrics at /v1/metrics.
// Also, it checks that the cache hit/miss metrics have been exposed accordingly.
func TestPrometheusMetrics(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
params := e2e.NewAPIServerTestParams()
params.Addrs = &[]string{"0.0.0.0:0"}
testRuntime, err := e2e.NewTestRuntime(params)
if err != nil {
t.Fatal(err)
}
done := make(chan bool)
go func() {
err := testRuntime.Runtime.Serve(ctx)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
done <- true
}()
t.Cleanup(cancel)
if err := testRuntime.WaitForServer(); err != nil {
t.Fatal(err)
}
opaURL := testRuntime.URL()
input := map[string]any{"fav_colour": "yellow"}
path := `filters/include`
policy := `package filters
# METADATA
# scope: document
# custom:
# unknowns: [input.fruits]
include if input.fruits.name == "banana"
`
{ // exercise the Compile API
req, err := http.NewRequest("PUT", opaURL+"/v1/policies/policy.rego", strings.NewReader(policy))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
if _, err := http.DefaultClient.Do(req); err != nil {
t.Fatalf("post policy: %v", err)
}
// query the compile API
payload := map[string]any{
"input": input,
}
queryBytes, err := json.Marshal(payload)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
for range 3 {
// POST to Compile API
req, err = http.NewRequest("POST",
opaURL+"/v1/compile/"+path,
strings.NewReader(string(queryBytes)))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/vnd.opa.sql.postgresql+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("failed to execute request: %v", err)
}
defer resp.Body.Close()
var respPayload struct {
Result struct {
Query any `json:"query"`
} `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&respPayload); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if exp, act := "WHERE fruits.name = E'banana'", respPayload.Result.Query; exp != act {
t.Errorf("response: expected %v, got %v (response: %v)", exp, act, respPayload)
}
}
}
{ // check the /v1/metrics endpoint for the handler invokation lines
needle := `http_request_duration_seconds_bucket{code="200",handler="v1/compile",method="post",`
act := findMetrics(t, opaURL, needle)
// NOTE(sr): The metric is a histogram, and we did multiple requests; so we only check
// for ANY lines being present, not their actual count.
if len(act) < 1 {
t.Errorf("expected v1/compile lines in metrics, got %v", act)
}
}
}
func findMetrics(t *testing.T, eopaURL string, needles ...string) []string {
t.Helper()
founds := []string{}
resp, err := http.Get(eopaURL + "/metrics")
if err != nil {
t.Fatalf("failed to send request: %v", err)
}
// search for line containing v1/compile
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
for i := range needles {
if strings.Contains(scanner.Text(), needles[i]) {
founds = append(founds, scanner.Text())
}
}
}
if err := scanner.Err(); err != nil {
t.Fatalf("failed to scan response: %v", err)
}
return founds
}
func getSQLAndRunQuery(t *testing.T, path string, payload map[string]any, config *TestConfig, opaURL string) []fruitRow {
t.Helper()
queryBytes, err := json.Marshal(payload)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
req, err := http.NewRequest("POST",
opaURL+"/v1/compile/"+path,
strings.NewReader(string(queryBytes)))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", fmt.Sprintf("application/vnd.opa.sql.%s+json", config.dbType))
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("failed to execute request: %v", err)
}
defer resp.Body.Close()
if status := resp.StatusCode; status != http.StatusOK {
t.Errorf("expected status %v, got %v", http.StatusOK, status)
}
var respPayload struct {
Result struct {
Query any `json:"query"`
} `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&respPayload); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
t.Log(respPayload)
var rowsData []fruitRow
var whereClauses string
switch w := respPayload.Result.Query.(type) {
case nil: // unconditional NO
return rowsData // empty
case string:
whereClauses = w
}
// finally, query the database with the resulting WHERE clauses
stmt := "SELECT * FROM fruit " + whereClauses
rows, err := config.db.Query(stmt)
if err != nil {
t.Fatalf("%s: error: %v", stmt, err)
}
// collect rows into rowsData
for rows.Next() {
var fruit fruitRow
// scan row into fruit, ignoring created_at
if err := rows.Scan(&fruit.ID, &fruit.Name, &fruit.Colour, &fruit.Price); err != nil {
t.Fatalf("failed to scan row: %v", err)
}
rowsData = append(rowsData, fruit)
}
return rowsData
}
func getUCASTAndRunPrisma(t *testing.T, path string, payload map[string]any, config *TestConfig, opaURL string) []fruitRow {
t.Helper()
queryBytes, err := json.Marshal(payload)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
// Query EOPA for UCAST IR
req, err := http.NewRequest("POST",
opaURL+"/v1/compile/"+path,
strings.NewReader(string(queryBytes)))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/vnd.opa.ucast.prisma+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("failed to execute request: %v", err)
}
defer resp.Body.Close()
if status := resp.StatusCode; status != http.StatusOK {
t.Errorf("expected status %v, got %v", http.StatusOK, status)
}
var respPayload struct {
Result struct {
Query map[string]any `json:"query"`
} `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&respPayload); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
t.Log("ucast IR:", respPayload.Result.Query)
// Execute prisma script with UCAST IR as input
cmd := exec.Command("node", "index.js")
cmd.Dir = "./prisma"
cmd.Env = append(cmd.Env, "DATABASE_URL="+config.dbURL)
stdin, err := cmd.StdinPipe()
if err != nil {
t.Fatalf("failed to get stdin pipe: %v", err)
}
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
t.Fatalf("failed to start prisma script: %v", err)
}
// Write UCAST IR to stdin
if err := json.NewEncoder(stdin).Encode(respPayload.Result.Query); err != nil {
t.Fatalf("failed to write to stdin: %v", err)
}
stdin.Close()
if err := cmd.Wait(); err != nil {
t.Fatalf("prisma script failed: %v\nstderr: %s", err, stderr.String())
}
t.Log("prisma query:", stderr.String())
// Parse the output into []fruitRow
if stdout.Len() == 0 {
return nil
}
var rowsData []fruitJSON
if err := json.NewDecoder(&stdout).Decode(&rowsData); err != nil {
t.Fatalf("failed to decode prisma output: %v\noutput was: %s", err, stdout.String())
}
return toFruitRows(rowsData)
}
+188
View File
@@ -0,0 +1,188 @@
// Copyright 2025 The OPA Authors
// SPDX-License-Identifier: Apache-2.0
package compile
import (
"context"
"encoding/json"
"fmt"
"net/http"
"slices"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/open-policy-agent/opa/v1/logging/test"
"github.com/open-policy-agent/opa/v1/runtime"
"github.com/open-policy-agent/opa/v1/test/e2e"
)
var ignores = []string{
"timestamp",
"metrics",
"labels",
"intermediate",
"requested_by",
}
var stdIgnores = cmpopts.IgnoreMapEntries(func(k string, _ any) bool {
return slices.Contains(ignores, k)
})
func TestDecisionLogsCompileAPIResult(t *testing.T) {
policy := `
package filters
# METADATA
# custom:
# unknowns: [input.fruits]
# mask_rule: data.filters.mask
include if input.fruits.name in input.favorites
default mask.fruits.supplier := {"replace": {"value": "***"}}
`
path := "filters/include"
params := e2e.NewAPIServerTestParams()
params.ConfigOverrides = []string{
"decision_logs.console=true",
}
params.Logging = runtime.LoggingConfig{Level: "error"}
consoleLogger := test.New()
params.ConsoleLogger = consoleLogger
testRuntime, err := e2e.NewTestRuntime(params)
if err != nil {
t.Fatal(err)
}
testRuntime.ConsoleLogger = consoleLogger
ctx, cancel := context.WithCancel(t.Context())
done := make(chan bool)
go func() {
err := testRuntime.Runtime.Serve(ctx)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
done <- true
}()
t.Cleanup(cancel)
if err := testRuntime.WaitForServer(); err != nil {
t.Fatal(err)
}
opaURL := testRuntime.URL()
{ // store policy
req, err := http.NewRequest("PUT", opaURL+"/v1/policies/policy.rego", strings.NewReader(policy))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
if _, err := http.DefaultClient.Do(req); err != nil {
t.Fatalf("put policy: %v", err)
}
}
{ // act: send Compile API request
input := map[string]any{"favorites": []string{"banana", "orange"}}
payload := map[string]any{
"input": input,
"options": map[string]any{
"targetSQLTableMappings": map[string]any{
"postgresql": map[string]any{
"fruits": map[string]string{
"$self": "f",
"name": "n",
},
},
},
},
}
queryBytes, err := json.Marshal(payload)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
req, err := http.NewRequest("POST",
fmt.Sprintf("%s/v1/compile/%s", opaURL, path),
strings.NewReader(string(queryBytes)))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/vnd.opa.sql.postgresql+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("failed to execute request: %v", err)
}
defer resp.Body.Close()
var respPayload struct {
Result struct {
Query any `json:"query"`
Masks map[string]any `json:"masks,omitempty"`
} `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&respPayload); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if exp, act := "WHERE f.n IN (E'banana', E'orange')", respPayload.Result.Query; exp != act {
t.Errorf("response: expected %v, got %v (response: %v)", exp, act, respPayload)
}
exp, act := map[string]any{"fruits": map[string]any{"supplier": map[string]any{"replace": map[string]any{"value": "***"}}}}, respPayload.Result.Masks
if diff := cmp.Diff(exp, act); diff != "" {
t.Errorf("response: expected %v, got %v (response: %v)", exp, act, respPayload)
}
}
var entry test.LogEntry
for _, entry = range testRuntime.ConsoleLogger.Entries() {
if entry.Message == "Decision Log" {
break
}
}
if entry.Message == "" {
t.Fatal("no DL messages logged")
}
dl := map[string]any{
"decision_id": "",
"path": path,
"result": map[string]any{
"query": "WHERE f.n IN (E'banana', E'orange')",
"masks": map[string]any{
"fruits": map[string]any{
"supplier": map[string]any{
"replace": map[string]any{"value": "***"},
},
},
},
},
"input": map[string]any{
"favorites": []any{"banana", "orange"},
},
"req_id": json.Number("3"),
"type": "openpolicyagent.org/decision_logs",
"custom": map[string]any{
"options": map[string]any{
"targetSQLTableMappings": map[string]any{
"postgresql": map[string]any{
"fruits": map[string]any{
"$self": "f",
"name": "n",
},
},
},
},
"unknowns": []any{"input.fruits"},
"type": "open-policy-agent/compile",
"mask_rule": "data.filters.mask",
},
}
if diff := cmp.Diff(dl, entry.Fields, stdIgnores); diff != "" {
t.Errorf("diff: (-want +got):\n%s", diff)
}
}
+1
View File
@@ -0,0 +1 @@
/node_modules
+26
View File
@@ -0,0 +1,26 @@
# E2E Prisma helper
This is a tiny script that is used to run our E2E tests via Prisma and [@open-policy-agent/ucast-prisma](https://www.npmjs.com/package/@open-policy-agent/ucast-prisma).
It's invoked by `e2e/compile/e2e_test.go` for a subset of the tests.
It reads UCAST conditions (as JSON) on STDIN, converts them to Prisma filters, and then runs
a "SELECT * FROM ..." query (findMany) against the `fruits` table like the other tests.
The returned rows are printed as JSON on STDOUT.
## Test a change
To use a local change to ucast-prisma with the Compile E2E suite, you'll have to do this:
In `package.json`, replace the line with "@open-policy-agent/ucast-prisma" with the following:
```json
"@open-policy-agent/ucast-prisma": "file:../../../opa-typescript/packages/ucast-prisma"
```
Also run `npm run build` in "opa-typescript/packages/ucast-prisma" to make sure the changes are built.
Run `npm i` in `e2e/prisma`, and then run the E2E tests for the prisma-enabled subset:
```
go test -tags e2e ./compile -run 'TestCompileHappyPathE2E/postgres/prisma/.' -v
```
+46
View File
@@ -0,0 +1,46 @@
/**
* Copyright 2025 The OPA Authors
* SPDX-License-Identifier: Apache-2.0
*/
const util = require("util");
const { PrismaClient } = require("@prisma/client");
const { ucastToPrisma } = require("@open-policy-agent/ucast-prisma");
const prisma = new PrismaClient();
// Function to read JSON from stdin
async function getStdinJson() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
const data = Buffer.concat(chunks).toString();
try {
return JSON.parse(data);
} catch (error) {
throw new Error("Invalid JSON input");
}
}
async function main() {
try {
const filters = await getStdinJson();
if (filters === null) return;
const where = ucastToPrisma(filters, "fruit", {
fruits: { $self: "fruit" },
});
console.error(util.inspect(where, { depth: null }));
const results = await prisma.fruit.findMany({ where });
process.stdout.write(JSON.stringify(results, null, 2));
} catch (error) {
console.error("Error:", error.message);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
// Run the script
main();
+456
View File
@@ -0,0 +1,456 @@
{
"name": "prisma-ucast-e2e",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "prisma-ucast-e2e",
"version": "0.0.1",
"dependencies": {
"@open-policy-agent/ucast-prisma": "^0.1.6",
"@prisma/client": "^6.15.0"
},
"devDependencies": {
"prisma": "^6.15.0"
}
},
"node_modules/@open-policy-agent/ucast-prisma": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@open-policy-agent/ucast-prisma/-/ucast-prisma-0.1.6.tgz",
"integrity": "sha512-4bnz9aZzQQyfoo6OrXCWA8IH6Kp6pB34e4A2vGv2B0S/dbfFVN8TzmUn+WTJg7Eyr/NiF2EPFShiFgBwbUhhrA==",
"license": "Apache-2.0",
"dependencies": {
"@ucast/core": "^1.10.1",
"lodash.merge": "^4.6.2"
}
},
"node_modules/@prisma/client": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.15.0.tgz",
"integrity": "sha512-wR2LXUbOH4cL/WToatI/Y2c7uzni76oNFND7+23ypLllBmIS8e3ZHhO+nud9iXSXKFt1SoM3fTZvHawg63emZw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
},
"peerDependencies": {
"prisma": "*",
"typescript": ">=5.1.0"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/@prisma/config": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.15.0.tgz",
"integrity": "sha512-KMEoec9b2u6zX0EbSEx/dRpx1oNLjqJEBZYyK0S3TTIbZ7GEGoVyGyFRk4C72+A38cuPLbfQGQvgOD+gBErKlA==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"c12": "3.1.0",
"deepmerge-ts": "7.1.5",
"effect": "3.16.12",
"empathic": "2.0.0"
}
},
"node_modules/@prisma/debug": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.15.0.tgz",
"integrity": "sha512-y7cSeLuQmyt+A3hstAs6tsuAiVXSnw9T55ra77z0nbNkA8Lcq9rNcQg6PI00by/+WnE/aMRJ/W7sZWn2cgIy1g==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.15.0.tgz",
"integrity": "sha512-opITiR5ddFJ1N2iqa7mkRlohCZqVSsHhRcc29QXeldMljOf4FSellLT0J5goVb64EzRTKcIDeIsJBgmilNcKxA==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.15.0",
"@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
"@prisma/fetch-engine": "6.15.0",
"@prisma/get-platform": "6.15.0"
}
},
"node_modules/@prisma/engines-version": {
"version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb.tgz",
"integrity": "sha512-a/46aK5j6L3ePwilZYEgYDPrhBQ/n4gYjLxT5YncUTJJNRnTCVjPF86QdzUOLRdYjCLfhtZp9aum90W0J+trrg==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.15.0.tgz",
"integrity": "sha512-xcT5f6b+OWBq6vTUnRCc7qL+Im570CtwvgSj+0MTSGA1o9UDSKZ/WANvwtiRXdbYWECpyC3CukoG3A04VTAPHw==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.15.0",
"@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
"@prisma/get-platform": "6.15.0"
}
},
"node_modules/@prisma/get-platform": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.15.0.tgz",
"integrity": "sha512-Jbb+Xbxyp05NSR1x2epabetHiXvpO8tdN2YNoWoA/ZsbYyxxu/CO/ROBauIFuMXs3Ti+W7N7SJtWsHGaWte9Rg==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.15.0"
}
},
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/@ucast/core": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz",
"integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==",
"license": "Apache-2.0"
},
"node_modules/c12": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
"integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.3",
"confbox": "^0.2.2",
"defu": "^6.1.4",
"dotenv": "^16.6.1",
"exsolve": "^1.0.7",
"giget": "^2.0.0",
"jiti": "^2.4.2",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"perfect-debounce": "^1.0.0",
"pkg-types": "^2.2.0",
"rc9": "^2.1.2"
},
"peerDependencies": {
"magicast": "^0.3.5"
},
"peerDependenciesMeta": {
"magicast": {
"optional": true
}
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/citty": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"consola": "^3.2.3"
}
},
"node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/consola": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": "^14.18.0 || >=16.10.0"
}
},
"node_modules/deepmerge-ts": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
"devOptional": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/destr": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"devOptional": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/effect": {
"version": "3.16.12",
"resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz",
"integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"fast-check": "^3.23.1"
}
},
"node_modules/empathic": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/exsolve": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/fast-check": {
"version": "3.23.2",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
"devOptional": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT",
"dependencies": {
"pure-rand": "^6.1.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/giget": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.4.0",
"defu": "^6.1.4",
"node-fetch-native": "^1.6.6",
"nypm": "^0.6.0",
"pathe": "^2.0.3"
},
"bin": {
"giget": "dist/cli.mjs"
}
},
"node_modules/jiti": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
"devOptional": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"license": "MIT"
},
"node_modules/node-fetch-native": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
"devOptional": true,
"license": "MIT"
},
"node_modules/nypm": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz",
"integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.4.2",
"pathe": "^2.0.3",
"pkg-types": "^2.2.0",
"tinyexec": "^1.0.1"
},
"bin": {
"nypm": "dist/cli.mjs"
},
"engines": {
"node": "^14.16.0 || >=16.10.0"
}
},
"node_modules/ohash": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"devOptional": true,
"license": "MIT"
},
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/pkg-types": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"confbox": "^0.2.2",
"exsolve": "^1.0.7",
"pathe": "^2.0.3"
}
},
"node_modules/prisma": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.15.0.tgz",
"integrity": "sha512-E6RCgOt+kUVtjtZgLQDBJ6md2tDItLJNExwI0XJeBc1FKL+Vwb+ovxXxuok9r8oBgsOXBA33fGDuE/0qDdCWqQ==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/config": "6.15.0",
"@prisma/engines": "6.15.0"
},
"bin": {
"prisma": "build/index.js"
},
"engines": {
"node": ">=18.18"
},
"peerDependencies": {
"typescript": ">=5.1.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/pure-rand": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
"devOptional": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT"
},
"node_modules/rc9": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"defu": "^6.1.4",
"destr": "^2.0.3"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/tinyexec": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
"integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
"devOptional": true,
"license": "MIT"
}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "prisma-ucast-e2e",
"version": "0.0.1",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "",
"description": "",
"dependencies": {
"@prisma/client": "^6.15.0",
"@open-policy-agent/ucast-prisma": "^0.1.6"
},
"devDependencies": {
"prisma": "^6.15.0"
}
}
@@ -0,0 +1,18 @@
generator client {
provider = "prisma-client-js"
}
// Turns out using multiple databases is actually not simple:
// https://github.com/prisma/prisma/issues/2443
// So let's test only PG for now, as it keeps the code generation simpler.
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model fruit {
id Int @id @default(autoincrement())
name String @db.VarChar(100)
colour String @db.VarChar(100)
price Int?
}