mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
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:
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/node_modules
|
||||
@@ -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
|
||||
```
|
||||
@@ -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();
|
||||
Generated
+456
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
Reference in New Issue
Block a user