From 8e19ed8459d1c1d741126088156d2d0c9dd20a6f Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Fri, 24 Jan 2025 11:24:31 -0800 Subject: [PATCH] feat: support MySQL database connections (#722) --- .trunk/configs/cspell.json | 2 + .vscode/launch.json | 4 + CHANGELOG.md | 1 + go.work | 1 + lib/manifest/manifest.go | 7 + lib/manifest/modus_schema.json | 18 + lib/manifest/mysql.go | 34 + lib/manifest/test/manifest_test.go | 20 + lib/manifest/test/valid_modus.json | 4 + runtime/go.mod | 4 + runtime/go.sum | 16 + runtime/services/services.go | 2 +- runtime/sqlclient/mysql.go | 207 + runtime/sqlclient/pooling.go | 78 - runtime/sqlclient/postgresql.go | 51 +- runtime/sqlclient/registry.go | 52 +- runtime/sqlclient/sqlclient.go | 72 +- runtime/sqlclient/types.go | 9 + sdk/assemblyscript/examples/mysql/.prettierrc | 3 + sdk/assemblyscript/examples/mysql/README.md | 5 + .../examples/mysql/asconfig.json | 6 + .../examples/mysql/assembly/index.ts | 104 + .../examples/mysql/assembly/tsconfig.json | 4 + .../examples/mysql/eslint.config.js | 11 + .../examples/mysql/extras/dbschema.sql | 6 + sdk/assemblyscript/examples/mysql/modus.json | 31 + .../examples/mysql/package-lock.json | 3545 +++++++++++++++++ .../examples/mysql/package.json | 33 + sdk/assemblyscript/src/assembly/database.ts | 160 +- sdk/assemblyscript/src/assembly/index.ts | 3 + sdk/assemblyscript/src/assembly/mysql.ts | 46 + sdk/assemblyscript/src/assembly/postgresql.ts | 147 +- sdk/go/examples/mysql/build.cmd | 12 + sdk/go/examples/mysql/build.sh | 12 + sdk/go/examples/mysql/go.mod | 7 + sdk/go/examples/mysql/go.sum | 2 + sdk/go/examples/mysql/main.go | 91 + sdk/go/examples/mysql/modus.json | 31 + sdk/go/examples/postgresql/main.go | 26 +- sdk/go/pkg/db/db.go | 123 +- sdk/go/pkg/db/db_test.go | 36 +- sdk/go/pkg/{postgresql => db}/location.go | 26 +- .../pkg/{postgresql => db}/location_test.go | 20 +- sdk/go/pkg/{postgresql => db}/point.go | 24 +- sdk/go/pkg/{postgresql => db}/point_test.go | 20 +- sdk/go/pkg/mysql/mysql.go | 53 + sdk/go/pkg/postgresql/postgresql.go | 35 +- 47 files changed, 4861 insertions(+), 343 deletions(-) create mode 100644 lib/manifest/mysql.go create mode 100644 runtime/sqlclient/mysql.go delete mode 100644 runtime/sqlclient/pooling.go create mode 100644 sdk/assemblyscript/examples/mysql/.prettierrc create mode 100644 sdk/assemblyscript/examples/mysql/README.md create mode 100644 sdk/assemblyscript/examples/mysql/asconfig.json create mode 100644 sdk/assemblyscript/examples/mysql/assembly/index.ts create mode 100644 sdk/assemblyscript/examples/mysql/assembly/tsconfig.json create mode 100644 sdk/assemblyscript/examples/mysql/eslint.config.js create mode 100644 sdk/assemblyscript/examples/mysql/extras/dbschema.sql create mode 100644 sdk/assemblyscript/examples/mysql/modus.json create mode 100644 sdk/assemblyscript/examples/mysql/package-lock.json create mode 100644 sdk/assemblyscript/examples/mysql/package.json create mode 100644 sdk/assemblyscript/src/assembly/mysql.ts create mode 100644 sdk/go/examples/mysql/build.cmd create mode 100755 sdk/go/examples/mysql/build.sh create mode 100644 sdk/go/examples/mysql/go.mod create mode 100644 sdk/go/examples/mysql/go.sum create mode 100644 sdk/go/examples/mysql/main.go create mode 100644 sdk/go/examples/mysql/modus.json rename sdk/go/pkg/{postgresql => db}/location.go (58%) rename sdk/go/pkg/{postgresql => db}/location_test.go (77%) rename sdk/go/pkg/{postgresql => db}/point.go (61%) rename sdk/go/pkg/{postgresql => db}/point_test.go (77%) create mode 100644 sdk/go/pkg/mysql/mysql.go diff --git a/.trunk/configs/cspell.json b/.trunk/configs/cspell.json index 6a2224284..f3e77e02e 100644 --- a/.trunk/configs/cspell.json +++ b/.trunk/configs/cspell.json @@ -33,6 +33,7 @@ "datasource", "dbname", "dbpool", + "dburl", "dcli", "Debugf", "dgraph", @@ -87,6 +88,7 @@ "jackc", "Jairus", "jensneuse", + "jmoiron", "joho", "jsonlogs", "jsonparser", diff --git a/.vscode/launch.json b/.vscode/launch.json index 2e9f0b265..fb65534ac 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -171,6 +171,10 @@ "label": "HTTP Client Example", "value": "http" }, + { + "label": "MySQL Client Example", + "value": "mysql" + }, { "label": "Neo4j Client Example", "value": "neo4j" diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ad703165..b49547add 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ [#707](https://github.com/hypermodeinc/modus/pull/707) - feat: support type aliases and redefinitions [#721](https://github.com/hypermodeinc/modus/pull/721) +- feat: support MySQL database connections [#722](https://github.com/hypermodeinc/modus/pull/722) ## 2025-01-09 - CLI 0.16.6 diff --git a/go.work b/go.work index dbe3310a9..dee556e17 100644 --- a/go.work +++ b/go.work @@ -17,6 +17,7 @@ use ( ./sdk/go/examples/embedding ./sdk/go/examples/graphql ./sdk/go/examples/http + ./sdk/go/examples/mysql ./sdk/go/examples/neo4j ./sdk/go/examples/postgresql ./sdk/go/examples/simple diff --git a/lib/manifest/manifest.go b/lib/manifest/manifest.go index fcdd74e31..07dc0382a 100644 --- a/lib/manifest/manifest.go +++ b/lib/manifest/manifest.go @@ -172,6 +172,13 @@ func parseManifestJson(data []byte, manifest *Manifest) error { } info.Name = name manifest.Connections[name] = info + case ConnectionTypeMysql: + var info MysqlConnectionInfo + if err := json.Unmarshal(rawCon, &info); err != nil { + return fmt.Errorf("failed to parse mysql connection [%s]: %w", name, err) + } + info.Name = name + manifest.Connections[name] = info case ConnectionTypeDgraph: var info DgraphConnectionInfo if err := json.Unmarshal(rawCon, &info); err != nil { diff --git a/lib/manifest/modus_schema.json b/lib/manifest/modus_schema.json index be8412a0c..3925d6fb8 100644 --- a/lib/manifest/modus_schema.json +++ b/lib/manifest/modus_schema.json @@ -219,6 +219,24 @@ "required": ["type", "connString"], "additionalProperties": false }, + { + "properties": { + "type": { + "type": "string", + "const": "mysql", + "description": "Type of the connection." + }, + "connString": { + "type": "string", + "minLength": 1, + "pattern": "^mysql:\\/\\/(.*?@)?([0-9a-zA-Z.-]*?)(:\\d+)?(\\/[0-9a-zA-Z.-]+)?(\\?.+)?$", + "description": "The MySQL connection string in URI format.", + "markdownDescription": "The MySQL connection string in URI format.\n\nReference: https://docs.hypermode.com/modus/app-manifest#mysql-connection" + } + }, + "required": ["type", "connString"], + "additionalProperties": false + }, { "properties": { "type": { diff --git a/lib/manifest/mysql.go b/lib/manifest/mysql.go new file mode 100644 index 000000000..f6b79e2f8 --- /dev/null +++ b/lib/manifest/mysql.go @@ -0,0 +1,34 @@ +/* + * Copyright 2025 Hypermode Inc. + * Licensed under the terms of the Apache License, Version 2.0 + * See the LICENSE file that accompanied this code for further details. + * + * SPDX-FileCopyrightText: 2025 Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package manifest + +const ConnectionTypeMysql ConnectionType = "mysql" + +type MysqlConnectionInfo struct { + Name string `json:"-"` + Type ConnectionType `json:"type"` + ConnStr string `json:"connString"` +} + +func (info MysqlConnectionInfo) ConnectionName() string { + return info.Name +} + +func (info MysqlConnectionInfo) ConnectionType() ConnectionType { + return info.Type +} + +func (info MysqlConnectionInfo) Hash() string { + return computeHash(info.Name, info.Type, info.ConnStr) +} + +func (info MysqlConnectionInfo) Variables() []string { + return extractVariables(info.ConnStr) +} diff --git a/lib/manifest/test/manifest_test.go b/lib/manifest/test/manifest_test.go index 3e843ef71..fc1975f91 100644 --- a/lib/manifest/test/manifest_test.go +++ b/lib/manifest/test/manifest_test.go @@ -97,6 +97,11 @@ func TestReadManifest(t *testing.T) { Type: manifest.ConnectionTypePostgresql, ConnStr: "postgresql://{{POSTGRESQL_USERNAME}}:{{POSTGRESQL_PASSWORD}}@1.2.3.4:5432/data?sslmode=disable", }, + "my-mysql": manifest.MysqlConnectionInfo{ + Name: "my-mysql", + Type: manifest.ConnectionTypeMysql, + ConnStr: "mysql://{{MYSQL_USERNAME}}:{{MYSQL_PASSWORD}}@1.2.3.4:3306/mydb?sslmode=disable", + }, "my-dgraph-cloud": manifest.DgraphConnectionInfo{ Name: "my-dgraph-cloud", Type: manifest.ConnectionTypeDgraph, @@ -207,6 +212,20 @@ func TestPostgresConnectionInfo_Hash(t *testing.T) { } } +func TestMysqlConnectionInfo_Hash(t *testing.T) { + connection := manifest.MysqlConnectionInfo{ + Name: "my-database", + ConnStr: "mysql://{{MYSQL_USERNAME}}:{{MYSQL_PASSWORD}}@1.2.3.4:3306/mydb?sslmode=disable", + } + + expectedHash := "3b96055cec5bd4195901e1442c856fe5b5493b0af0dde8f64f1d14a4795f5272" + + actualHash := connection.Hash() + if actualHash != expectedHash { + t.Errorf("Expected hash: %s, but got: %s", expectedHash, actualHash) + } +} + func TestDgraphCloudConnectionInfo_Hash(t *testing.T) { connection := manifest.DgraphConnectionInfo{ Name: "my-dgraph-cloud", @@ -259,6 +278,7 @@ func TestGetVariablesFromManifest(t *testing.T) { "my-rest-api": {"API_TOKEN"}, "another-rest-api": {"USERNAME", "PASSWORD"}, "neon": {"POSTGRESQL_USERNAME", "POSTGRESQL_PASSWORD"}, + "my-mysql": {"MYSQL_USERNAME", "MYSQL_PASSWORD"}, "my-dgraph-cloud": {"DGRAPH_KEY"}, "my-neo4j": {"NEO4J_USERNAME", "NEO4J_PASSWORD"}, } diff --git a/lib/manifest/test/valid_modus.json b/lib/manifest/test/valid_modus.json index fcf188853..9c9ebb008 100644 --- a/lib/manifest/test/valid_modus.json +++ b/lib/manifest/test/valid_modus.json @@ -64,6 +64,10 @@ "type": "postgresql", "connString": "postgresql://{{POSTGRESQL_USERNAME}}:{{POSTGRESQL_PASSWORD}}@1.2.3.4:5432/data?sslmode=disable" }, + "my-mysql": { + "type": "mysql", + "connString": "mysql://{{MYSQL_USERNAME}}:{{MYSQL_PASSWORD}}@1.2.3.4:3306/mydb?sslmode=disable" + }, "my-dgraph-cloud": { "type": "dgraph", "grpcTarget": "frozen-mango.grpc.eu-central-1.aws.cloud.dgraph.io:443", diff --git a/runtime/go.mod b/runtime/go.mod index 0ed4cfdd1..0e3ed9935 100644 --- a/runtime/go.mod +++ b/runtime/go.mod @@ -24,6 +24,7 @@ require ( github.com/docker/go-connections v0.5.0 github.com/fatih/color v1.18.0 github.com/getsentry/sentry-go v0.31.1 + github.com/go-sql-driver/mysql v1.8.1 github.com/go-viper/mapstructure/v2 v2.2.1 github.com/goccy/go-json v0.10.4 github.com/gofrs/flock v0.12.1 @@ -46,15 +47,18 @@ require ( github.com/tetratelabs/wazero v1.8.2 github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 + github.com/twpayne/go-geom v1.6.0 github.com/viterin/vek v0.4.2 github.com/wundergraph/graphql-go-tools/execution v1.2.0 github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.142 + github.com/xo/dburl v0.23.2 golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 golang.org/x/sys v0.29.0 google.golang.org/grpc v1.69.4 ) require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.54 // indirect diff --git a/runtime/go.sum b/runtime/go.sum index 0edaa2838..aaeca93c2 100644 --- a/runtime/go.sum +++ b/runtime/go.sum @@ -1,8 +1,12 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/99designs/gqlgen v0.17.49 h1:b3hNGexHd33fBSAd4NDT/c3NCcQzcAVkknhN9ym36YQ= github.com/99designs/gqlgen v0.17.49/go.mod h1:tC8YFVZMed81x7UJ7ORUwXF4Kn6SXuucFqQBhN8+BU0= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -11,6 +15,10 @@ github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8 github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= +github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/archdx/zerolog-sentry v1.8.5 h1:W24e5+yfZiQ83yd9OjBw+o6ERUzyUlCpoBS97gUlwK8= github.com/archdx/zerolog-sentry v1.8.5/go.mod h1:XrFHGe1CH5DQk/XSySu/IJSi5C9XR6+zpc97zVf/c4c= github.com/aws/aws-sdk-go-v2 v1.33.0 h1:Evgm4DI9imD81V0WwD+TN4DCwjUMdc94TrduMLbgZJs= @@ -100,6 +108,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -138,6 +148,8 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hypermodeinc/modus/lib/manifest v0.16.1 h1:/37OgLlRhn9UNxNChzCUSDNNUqWHyRliPxQ1EoeQrRM= github.com/hypermodeinc/modus/lib/manifest v0.16.1/go.mod h1:NaG6aE+ekaufwqblbd70t/s1urmAQjNPL1nB4YhM27E= github.com/hypermodeinc/modus/lib/metadata v0.15.0 h1:Qu75TZg7l43Fi61EhnjasTHZvztrGA90vzDLnCB6ILI= @@ -282,6 +294,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/twpayne/go-geom v1.6.0 h1:WPOJLCdd8OdcnHvKQepLKwOZrn5BzVlNxtQB59IDHRE= +github.com/twpayne/go-geom v1.6.0/go.mod h1:Kr+Nly6BswFsKM5sd31YaoWS5PeDDH2NftJTK7Gd028= github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8= github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww= github.com/viterin/partial v1.1.0 h1:iH1l1xqBlapXsYzADS1dcbizg3iQUKTU1rbwkHv/80E= @@ -298,6 +312,8 @@ github.com/wundergraph/graphql-go-tools/execution v1.2.0 h1:9PXcNSN2n231q/YZZS3k github.com/wundergraph/graphql-go-tools/execution v1.2.0/go.mod h1:sv2LtqCiTCdiK0P6x3KUYLb9C1V8RW9H/9eqEdfgktY= github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.142 h1:CNuk0zoqmoJVP9Wq03GWLvi64Vpq1qwBIdRgV1669U8= github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.142/go.mod h1:B7eV0Qh8Lop9QzIOQcsvKp3S0ejfC6mgyWoJnI917yQ= +github.com/xo/dburl v0.23.2 h1:Fl88cvayrgE56JA/sqhNMLljCW/b7RmG1mMkKMZUFgA= +github.com/xo/dburl v0.23.2/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= diff --git a/runtime/services/services.go b/runtime/services/services.go index 419d24362..e1e8a7087 100644 --- a/runtime/services/services.go +++ b/runtime/services/services.go @@ -76,7 +76,7 @@ func Stop(ctx context.Context) { collections.Shutdown(ctx) middleware.Shutdown() - sqlclient.ShutdownPGPools() + sqlclient.Shutdown() dgraphclient.ShutdownConns() neo4jclient.CloseDrivers(ctx) logger.Close() diff --git a/runtime/sqlclient/mysql.go b/runtime/sqlclient/mysql.go new file mode 100644 index 000000000..af8d4a3ec --- /dev/null +++ b/runtime/sqlclient/mysql.go @@ -0,0 +1,207 @@ +/* + * Copyright 2025 Hypermode Inc. + * Licensed under the terms of the Apache License, Version 2.0 + * See the LICENSE file that accompanied this code for further details. + * + * SPDX-FileCopyrightText: 2025 Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package sqlclient + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "time" + + "github.com/hypermodeinc/modus/lib/manifest" + "github.com/hypermodeinc/modus/runtime/logger" + "github.com/hypermodeinc/modus/runtime/utils" + + "github.com/go-sql-driver/mysql" + "github.com/twpayne/go-geom/encoding/wkb" + "github.com/twpayne/go-geom/encoding/wkt" + "github.com/xo/dburl" +) + +type mysqlDS struct { + pool *sql.DB +} + +func (ds *mysqlDS) Shutdown() { + ds.pool.Close() +} + +func (ds *mysqlDS) query(ctx context.Context, stmt string, params []any, execOnly bool) (*dbResponse, error) { + + tx, err := ds.pool.BeginTx(ctx, nil) + if err != nil { + return nil, fmt.Errorf("error setting up a new tx: %w", err) + } + defer func() { + if err := tx.Rollback(); err != nil && err != sql.ErrTxDone { + logger.Warn(ctx).Err(err).Msg("Error rolling back transaction.") + return + } + }() + + // TODO: what if connection times out and we need to retry + + response := new(dbResponse) + if execOnly { + if res, err := tx.ExecContext(ctx, stmt, params...); err != nil { + return nil, err + } else if ra, err := res.RowsAffected(); err == nil { + response.RowsAffected = uint32(ra) + if id, err := res.LastInsertId(); err == nil { + response.LastInsertID = uint64(id) + } + } + } else { + rows, err := tx.QueryContext(ctx, stmt, params...) + if err != nil { + return nil, err + } + + data, err := rowsToMap(rows) + if err != nil { + return nil, err + } + + response.Result = data + response.RowsAffected = uint32(len(data)) + } + + if err := tx.Commit(); err != nil { + return nil, err + } + return response, nil +} + +func newMysqlDS(ctx context.Context, dsName string) (*mysqlDS, error) { + connStr, err := getConnectionString(ctx, dsName, manifest.ConnectionTypeMysql) + if err != nil { + return nil, err + } + + // Note: We use "dburl" to support URL-Like connection strings. + // See: + // - https://github.com/xo/dburl/blob/master/README.md + // - https://dev.mysql.com/doc/refman/8.4/en/connecting-using-uri-or-key-value-pairs.html#connecting-using-uri + url, err := dburl.Parse(connStr) + if err != nil { + return nil, fmt.Errorf("failed to parse mysql connection URL: %w", err) + } + cfg, err := mysql.ParseDSN(url.DSN) + if err != nil { + return nil, fmt.Errorf("failed to parse mysql connection DSN: %w", err) + } + + // Set some default options that make sense for Modus apps. + // See: https://github.com/go-sql-driver/mysql/blob/master/README.md#parameters + q := url.Query() + if !q.Has("tls") { + cfg.TLSConfig = "preferred" + } + if !q.Has("clientFoundRows") { + cfg.ClientFoundRows = true + } + + conn, err := mysql.NewConnector(cfg) + if err != nil { + return nil, fmt.Errorf("failed to create mysql connector: %w", err) + } + + // Note: this doesn't actually open a connection to the database. + // It establishes a connection pool that will be used to create connections. + db := sql.OpenDB(conn) + + // TODO: We may want to make these configurable. + // Reference: https://github.com/go-sql-driver/mysql/blob/master/README.md#important-settings + db.SetConnMaxLifetime(time.Minute * 3) + db.SetMaxOpenConns(10) + db.SetMaxIdleConns(10) + + // Try to connect to the database to ensure the connection is valid. + if err := db.PingContext(ctx); err != nil { + return nil, fmt.Errorf("failed to connect mysql database [%s]: %w", dsName, err) + } + + return &mysqlDS{db}, nil +} + +func rowsToMap(rows *sql.Rows) ([]map[string]any, error) { + colTypes, err := rows.ColumnTypes() + if err != nil { + return nil, fmt.Errorf("error getting column types: %w", err) + } + + rawValues := make([]any, len(colTypes)) + for i, ct := range colTypes { + switch ct.DatabaseTypeName() { + case "DATE", "DATETIME", "TIMESTAMP": + rawValues[i] = new([]byte) + default: + rawValues[i] = reflect.New(ct.ScanType()).Interface() + } + } + + var data []map[string]any + for rows.Next() { + if err := rows.Scan(rawValues...); err != nil { + return nil, fmt.Errorf("error scanning row: %w", err) + } + + m := make(map[string]any, len(colTypes)) + for i, ct := range colTypes { + switch ct.DatabaseTypeName() { + case "DATE": + b := *rawValues[i].(*[]byte) + if b == nil { + m[ct.Name()] = nil + continue + } + m[ct.Name()] = string(b) + case "DATETIME": + b := *rawValues[i].(*[]byte) + if b == nil { + m[ct.Name()] = nil + continue + } + b[10] = 'T' // replace space with T + m[ct.Name()] = string(b) + case "TIMESTAMP": + b := *rawValues[i].(*[]byte) + if b == nil { + m[ct.Name()] = nil + continue + } + b[10] = 'T' // replace space with T + b = append(b, 'Z') // add Z to indicate UTC + m[ct.Name()] = string(b) + case "GEOMETRY": + b := *rawValues[i].(*[]byte) + if b == nil { + m[ct.Name()] = nil + continue + } + geom, err := wkb.Unmarshal(b[4:]) // skip the 4-byte SRID + if err != nil { + return nil, fmt.Errorf("error unmarshalling geometry from WKB: %w", err) + } + wkt, err := wkt.Marshal(geom) + if err != nil { + return nil, fmt.Errorf("error marshalling geometry to WKT: %w", err) + } + m[ct.Name()] = wkt + default: + m[ct.Name()] = utils.DereferencePointer(rawValues[i]) + } + } + data = append(data, m) + } + + return data, nil +} diff --git a/runtime/sqlclient/pooling.go b/runtime/sqlclient/pooling.go deleted file mode 100644 index 8dd446989..000000000 --- a/runtime/sqlclient/pooling.go +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2024 Hypermode Inc. - * Licensed under the terms of the Apache License, Version 2.0 - * See the LICENSE file that accompanied this code for further details. - * - * SPDX-FileCopyrightText: 2024 Hypermode Inc. - * SPDX-License-Identifier: Apache-2.0 - */ - -package sqlclient - -import ( - "context" - "fmt" - - "github.com/hypermodeinc/modus/lib/manifest" - "github.com/hypermodeinc/modus/runtime/manifestdata" - "github.com/hypermodeinc/modus/runtime/secrets" - "github.com/jackc/pgx/v5/pgxpool" -) - -// ShutdownPGPools shuts down all the PostgreSQL connection pools. -func ShutdownPGPools() { - dsr.cache.Range(func(key string, _ *postgresqlDS) bool { - if ds, ok := dsr.cache.LoadAndDelete(key); ok { - ds.pool.Close() - } - return true - }) -} - -func (r *dsRegistry) getPostgresDS(ctx context.Context, dsName string) (*postgresqlDS, error) { - var creationErr error - ds, _ := r.cache.LoadOrCompute(dsName, func() *postgresqlDS { - ds, err := createDS(ctx, dsName) - if err != nil { - creationErr = err - return nil - } - return ds - }) - - if creationErr != nil { - r.cache.Delete(dsName) - return nil, creationErr - } - - return ds, nil -} - -func createDS(ctx context.Context, dsName string) (*postgresqlDS, error) { - man := manifestdata.GetManifest() - info, ok := man.Connections[dsName] - if !ok { - return nil, fmt.Errorf("postgresql connection [%s] not found", dsName) - } - - if info.ConnectionType() != manifest.ConnectionTypePostgresql { - return nil, fmt.Errorf("[%s] is not a postgresql connection", dsName) - } - - conf := info.(manifest.PostgresqlConnectionInfo) - if conf.ConnStr == "" { - return nil, fmt.Errorf("postgresql connection [%s] has empty connString", dsName) - } - - connStr, err := secrets.ApplySecretsToString(ctx, info, conf.ConnStr) - if err != nil { - return nil, fmt.Errorf("failed to apply secrets to connection string for connection [%s]: %w", dsName, err) - } - - pool, err := pgxpool.New(ctx, connStr) - if err != nil { - return nil, fmt.Errorf("failed to connect to postgres connection [%s]: %w", dsName, err) - } - - return &postgresqlDS{pool}, nil -} diff --git a/runtime/sqlclient/postgresql.go b/runtime/sqlclient/postgresql.go index 2ced680e5..92fe1a0c3 100644 --- a/runtime/sqlclient/postgresql.go +++ b/runtime/sqlclient/postgresql.go @@ -13,6 +13,7 @@ import ( "context" "fmt" + "github.com/hypermodeinc/modus/lib/manifest" "github.com/hypermodeinc/modus/runtime/logger" "github.com/jackc/pgx/v5" @@ -23,7 +24,11 @@ type postgresqlDS struct { pool *pgxpool.Pool } -func (ds *postgresqlDS) query(ctx context.Context, stmt string, params []any) (*dbResponse, error) { +func (ds *postgresqlDS) Shutdown() { + ds.pool.Close() +} + +func (ds *postgresqlDS) query(ctx context.Context, stmt string, params []any, execOnly bool) (*dbResponse, error) { tx, err := ds.pool.Begin(ctx) if err != nil { @@ -37,27 +42,45 @@ func (ds *postgresqlDS) query(ctx context.Context, stmt string, params []any) (* }() // TODO: what if connection times out and we need to retry - rows, err := tx.Query(ctx, stmt, params...) - if err != nil { - return nil, err + + response := new(dbResponse) + if execOnly { + if ct, err := tx.Exec(ctx, stmt, params...); err != nil { + return nil, err + } else { + response.RowsAffected = uint32(ct.RowsAffected()) + } + } else { + rows, err := tx.Query(ctx, stmt, params...) + if err != nil { + return nil, err + } + + if data, err := pgx.CollectRows(rows, pgx.RowToMap); err != nil { + return nil, err + } else { + response.Result = data + } + + response.RowsAffected = uint32(rows.CommandTag().RowsAffected()) } - data, err := pgx.CollectRows(rows, pgx.RowToMap) - if err != nil { + if err := tx.Commit(ctx); err != nil { return nil, err } + return response, nil +} - rowsAffected := uint32(rows.CommandTag().RowsAffected()) - - if err := tx.Commit(ctx); err != nil { +func newPostgresqlDS(ctx context.Context, dsName string) (*postgresqlDS, error) { + connStr, err := getConnectionString(ctx, dsName, manifest.ConnectionTypePostgresql) + if err != nil { return nil, err } - response := &dbResponse{ - // Error: "", - Result: data, - RowsAffected: rowsAffected, + pool, err := pgxpool.New(ctx, connStr) + if err != nil { + return nil, fmt.Errorf("failed to connect to postgresql database [%s]: %w", dsName, err) } - return response, nil + return &postgresqlDS{pool}, nil } diff --git a/runtime/sqlclient/registry.go b/runtime/sqlclient/registry.go index 7aa013688..885c67618 100644 --- a/runtime/sqlclient/registry.go +++ b/runtime/sqlclient/registry.go @@ -9,16 +9,62 @@ package sqlclient -import "github.com/puzpuzpuz/xsync/v3" +import ( + "context" + "fmt" + + "github.com/puzpuzpuz/xsync/v3" +) var dsr = newDSRegistry() type dsRegistry struct { - cache *xsync.MapOf[string, *postgresqlDS] + cache *xsync.MapOf[string, dataSource] } func newDSRegistry() *dsRegistry { return &dsRegistry{ - cache: xsync.NewMapOf[string, *postgresqlDS](), + cache: xsync.NewMapOf[string, dataSource](), + } +} + +func (r *dsRegistry) shutdown() { + r.cache.Range(func(key string, _ dataSource) bool { + if ds, ok := dsr.cache.LoadAndDelete(key); ok { + ds.Shutdown() + } + return true + }) +} + +func (r *dsRegistry) getDataSource(ctx context.Context, dsName, dsType string) (dataSource, error) { + var creationErr error + ds, _ := r.cache.LoadOrCompute(dsName, func() dataSource { + switch dsType { + case "postgresql": + if ds, err := newPostgresqlDS(ctx, dsName); err != nil { + creationErr = err + return nil + } else { + return ds + } + case "mysql": + if ds, err := newMysqlDS(ctx, dsName); err != nil { + creationErr = err + return nil + } else { + return ds + } + default: + creationErr = fmt.Errorf("unsupported data source type: %s", dsType) + return nil + } + }) + + if creationErr != nil { + r.cache.Delete(dsName) + return nil, creationErr } + + return ds, nil } diff --git a/runtime/sqlclient/sqlclient.go b/runtime/sqlclient/sqlclient.go index 4f25751c9..217e1e6d2 100644 --- a/runtime/sqlclient/sqlclient.go +++ b/runtime/sqlclient/sqlclient.go @@ -12,25 +12,45 @@ package sqlclient import ( "context" "fmt" + "strings" + "github.com/hypermodeinc/modus/lib/manifest" "github.com/hypermodeinc/modus/runtime/manifestdata" + "github.com/hypermodeinc/modus/runtime/secrets" "github.com/hypermodeinc/modus/runtime/utils" ) func Initialize() { manifestdata.RegisterManifestLoadedCallback(func(ctx context.Context) error { - ShutdownPGPools() + dsr.shutdown() return nil }) } +func Shutdown() { + dsr.shutdown() +} + func ExecuteQuery(ctx context.Context, connectionName, dbType, statement, paramsJson string) (*HostQueryResponse, error) { + + // Small Hack: + // The `paramsJson` argument might be prefixed with flags set by the SDK. + // Example: "flag1,flag2:[]" (where `[]` is the actual JSON string) + // + // Currently supported flags: + // - "exec" - No rows are expected to be returned. Just execute the provided statement. + // + // Passing the flags in this manner avoids a breaking change in the host function signature. + // + var execOnly bool + paramsJson, execOnly = strings.CutPrefix(paramsJson, "exec:") + var params []any if err := utils.JsonDeserialize([]byte(paramsJson), ¶ms); err != nil { return nil, fmt.Errorf("error deserializing database query parameters: %w", err) } - dbResponse, err := doExecuteQuery(ctx, connectionName, dbType, statement, params) + dbResponse, err := doExecuteQuery(ctx, connectionName, dbType, statement, params, execOnly) if err != nil { return nil, err } @@ -47,6 +67,7 @@ func ExecuteQuery(ctx context.Context, connectionName, dbType, statement, params response := &HostQueryResponse{ Error: dbResponse.Error, RowsAffected: dbResponse.RowsAffected, + LastInsertID: dbResponse.LastInsertID, } if len(resultJson) > 0 { @@ -57,17 +78,44 @@ func ExecuteQuery(ctx context.Context, connectionName, dbType, statement, params return response, nil } -func doExecuteQuery(ctx context.Context, dsName, dsType, stmt string, params []any) (*dbResponse, error) { - switch dsType { - case "postgresql": - ds, err := dsr.getPostgresDS(ctx, dsName) - if err != nil { - return nil, err - } +func doExecuteQuery(ctx context.Context, dsName, dsType, stmt string, params []any, execOnly bool) (*dbResponse, error) { + ds, err := dsr.getDataSource(ctx, dsName, dsType) + if err != nil { + return nil, err + } - return ds.query(ctx, stmt, params) + return ds.query(ctx, stmt, params, execOnly) +} + +func getConnectionString(ctx context.Context, dsName string, connType manifest.ConnectionType) (string, error) { + man := manifestdata.GetManifest() + if man == nil { + return "", fmt.Errorf("manifest not loaded") + } + + info, ok := man.Connections[dsName] + if !ok { + return "", fmt.Errorf("connection [%s] not found", dsName) + } + if info.ConnectionType() != connType { + return "", fmt.Errorf("[%s] is not a %s connection", dsName, connType) + } + + var connStr string + switch t := info.(type) { + case manifest.MysqlConnectionInfo: + connStr = t.ConnStr + case manifest.PostgresqlConnectionInfo: + connStr = t.ConnStr + } + + if connStr == "" { + return "", fmt.Errorf("connection [%s] has empty or missing connection string", dsName) + } - default: - return nil, fmt.Errorf("connection [%s] has an unsupported type: %s", dsName, dsType) + if cs, err := secrets.ApplySecretsToString(ctx, info, connStr); err != nil { + return "", fmt.Errorf("failed to apply secrets for connection [%s]: %w", dsName, err) + } else { + return cs, nil } } diff --git a/runtime/sqlclient/types.go b/runtime/sqlclient/types.go index d1f16fb44..456e86545 100644 --- a/runtime/sqlclient/types.go +++ b/runtime/sqlclient/types.go @@ -9,14 +9,23 @@ package sqlclient +import "context" + +type dataSource interface { + Shutdown() + query(ctx context.Context, stmt string, params []any, execOnly bool) (*dbResponse, error) +} + type dbResponse struct { Error *string Result any RowsAffected uint32 + LastInsertID uint64 } type HostQueryResponse struct { Error *string ResultJson *string RowsAffected uint32 + LastInsertID uint64 } diff --git a/sdk/assemblyscript/examples/mysql/.prettierrc b/sdk/assemblyscript/examples/mysql/.prettierrc new file mode 100644 index 000000000..64cb35ca8 --- /dev/null +++ b/sdk/assemblyscript/examples/mysql/.prettierrc @@ -0,0 +1,3 @@ +{ + "plugins": ["assemblyscript-prettier"] +} diff --git a/sdk/assemblyscript/examples/mysql/README.md b/sdk/assemblyscript/examples/mysql/README.md new file mode 100644 index 000000000..542473518 --- /dev/null +++ b/sdk/assemblyscript/examples/mysql/README.md @@ -0,0 +1,5 @@ +# Modus MySQL Example + +This example shows how to access an external MySQL database. + +See [./assembly/index.ts](./assembly/index.ts) for the implementation. diff --git a/sdk/assemblyscript/examples/mysql/asconfig.json b/sdk/assemblyscript/examples/mysql/asconfig.json new file mode 100644 index 000000000..d8372651f --- /dev/null +++ b/sdk/assemblyscript/examples/mysql/asconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "./node_modules/@hypermode/modus-sdk-as/plugin.asconfig.json", + "options": { + "transform": ["@hypermode/modus-sdk-as/transform", "json-as/transform"] + } +} diff --git a/sdk/assemblyscript/examples/mysql/assembly/index.ts b/sdk/assemblyscript/examples/mysql/assembly/index.ts new file mode 100644 index 000000000..2775682e5 --- /dev/null +++ b/sdk/assemblyscript/examples/mysql/assembly/index.ts @@ -0,0 +1,104 @@ +/* + * This example is part of the Modus project, licensed under the Apache License 2.0. + * You may modify and use this example in accordance with the license. + * See the LICENSE file that accompanied this code for further details. + */ + +import { mysql } from "@hypermode/modus-sdk-as"; + +// The name of the MySQL host, as specified in the modus.json manifest +const host = "my-database"; + + +@json +class Person { + id: i32 = 0; + name!: string; + age!: i32; + home!: mysql.Location | null; +} + +export function getAllPeople(): Person[] { + const query = "select * from people order by id"; + const response = mysql.query(host, query); + return response.rows; +} + +export function getPeopleByName(name: string): Person[] { + const query = "select * from people where name = ?"; + + const params = new mysql.Params(); + params.push(name); + + const response = mysql.query(host, query, params); + return response.rows; +} + +export function getPerson(id: i32): Person | null { + const query = "select * from people where id = ?"; + + const params = new mysql.Params(); + params.push(id); + + const response = mysql.query(host, query, params); + return response.rows.length > 0 ? response.rows[0] : null; +} + +export function addPerson(name: string, age: i32): Person { + const query = "insert into people (name, age) values (?, ?)"; + + const params = new mysql.Params(); + params.push(name); + params.push(age); + + const response = mysql.execute(host, query, params); + + if (response.rowsAffected != 1) { + throw new Error("Failed to insert person."); + } + + const id = response.lastInsertId; + return { id, name, age }; +} + +export function updatePersonHome( + id: i32, + longitude: f64, + latitude: f64, +): Person | null { + const query = `update people set home = point(?,?) where id = ?`; + + const params = new mysql.Params(); + params.push(longitude); + params.push(latitude); + params.push(id); + + const response = mysql.execute(host, query, params); + + if (response.rowsAffected != 1) { + console.error( + `Failed to update person with id ${id}. The record may not exist.`, + ); + return null; + } + + return getPerson(id); +} + +export function deletePerson(id: i32): string { + const query = "delete from people where id = ?"; + + const params = new mysql.Params(); + params.push(id); + + const response = mysql.execute(host, query, params); + + if (response.rowsAffected != 1) { + console.error( + `Failed to delete person with id ${id}. The record may not exist.`, + ); + return "failure"; + } + + return "success"; +} diff --git a/sdk/assemblyscript/examples/mysql/assembly/tsconfig.json b/sdk/assemblyscript/examples/mysql/assembly/tsconfig.json new file mode 100644 index 000000000..798b474ea --- /dev/null +++ b/sdk/assemblyscript/examples/mysql/assembly/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "assemblyscript/std/assembly.json", + "include": ["./**/*.ts"] +} diff --git a/sdk/assemblyscript/examples/mysql/eslint.config.js b/sdk/assemblyscript/examples/mysql/eslint.config.js new file mode 100644 index 000000000..7ad50aead --- /dev/null +++ b/sdk/assemblyscript/examples/mysql/eslint.config.js @@ -0,0 +1,11 @@ +// @ts-check + +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import aseslint from "@hypermode/modus-sdk-as/tools/assemblyscript-eslint"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + aseslint.config, +); diff --git a/sdk/assemblyscript/examples/mysql/extras/dbschema.sql b/sdk/assemblyscript/examples/mysql/extras/dbschema.sql new file mode 100644 index 000000000..a42bbd715 --- /dev/null +++ b/sdk/assemblyscript/examples/mysql/extras/dbschema.sql @@ -0,0 +1,6 @@ +CREATE TABLE people ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + name TEXT NOT NULL, + age INT NOT NULL, + home POINT NULL +); diff --git a/sdk/assemblyscript/examples/mysql/modus.json b/sdk/assemblyscript/examples/mysql/modus.json new file mode 100644 index 000000000..064a4d04a --- /dev/null +++ b/sdk/assemblyscript/examples/mysql/modus.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://schema.hypermode.com/modus.json", + "endpoints": { + "default": { + "type": "graphql", + "path": "/graphql", + "auth": "bearer-token" + } + }, + "connections": { + // This example shows how you can set a host that references a MySQL database. + // The connection string should be set using URI format. See the MySQL documentation for more information: + // https://dev.mysql.com/doc/refman/8.4/en/connecting-using-uri-or-key-value-pairs.html#connecting-using-uri + // + // However, any optional parameters provided should be in the form specified here: + // https://github.com/go-sql-driver/mysql/blob/master/README.md#parameters + // + // For example, use tls=true to enable encryption (not sslmode=require) + // + // Where secrets are required, {{SECRET_NAME}} templates are replaced with the values + // specified in the Hypermode Console. Do not include actual secret values in this file. + + "my-database": { + "type": "mysql", + "connString": "mysql://{{USERNAME}}:{{PASSWORD}}@database.example.com:3306/dbname?tls=true" + + // For testing with a local db instance, replace the above line with the following: + // "connString": "mysql://root@localhost/mydb" + } + } +} diff --git a/sdk/assemblyscript/examples/mysql/package-lock.json b/sdk/assemblyscript/examples/mysql/package-lock.json new file mode 100644 index 000000000..b9d2fd79d --- /dev/null +++ b/sdk/assemblyscript/examples/mysql/package-lock.json @@ -0,0 +1,3545 @@ +{ + "name": "mysql-example", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mysql-example", + "license": "Apache-2.0", + "dependencies": { + "@hypermode/modus-sdk-as": "../../src", + "json-as": "^0.9.28" + }, + "devDependencies": { + "@eslint/js": "^9.18.0", + "@types/eslint__js": "^8.42.3", + "@typescript-eslint/parser": "^8.21.0", + "assemblyscript": "^0.27.32", + "assemblyscript-prettier": "^3.0.1", + "eslint": "^9.18.0", + "prettier": "^3.4.2", + "typescript": "^5.7.3", + "typescript-eslint": "^8.21.0", + "visitor-as": "^0.11.4" + } + }, + "../../src": { + "name": "@hypermode/modus-sdk-as", + "license": "Apache-2.0", + "dependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "as-base64": "^0.2.0", + "chalk": "^5.4.1", + "json-as": "^0.9.28", + "semver": "^7.6.3", + "xid-ts": "^1.1.4" + }, + "bin": { + "modus-as-build": "bin/build-plugin.js" + }, + "devDependencies": { + "@eslint/js": "^9.18.0", + "@types/eslint__js": "^8.42.3", + "@types/node": "^22.10.7", + "as-test": "^0.3.5", + "assemblyscript": "^0.27.32", + "assemblyscript-prettier": "^3.0.1", + "eslint": "^9.18.0", + "prettier": "^3.4.2", + "typescript": "^5.7.3", + "typescript-eslint": "^8.21.0", + "visitor-as": "^0.11.4" + }, + "engines": { + "node": ">=22" + } + }, + "../../src/node_modules/@assemblyscript/wasi-shim": { + "version": "0.1.0", + "license": "Apache-2.0", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/assemblyscript" + } + }, + "../../src/node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "../../src/node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "../../src/node_modules/@eslint/config-array": { + "version": "0.19.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../../src/node_modules/@eslint/core": { + "version": "0.9.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../../src/node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../src/node_modules/@eslint/js": { + "version": "9.15.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../../src/node_modules/@eslint/object-schema": { + "version": "2.1.4", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../../src/node_modules/@eslint/plugin-kit": { + "version": "0.2.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../../src/node_modules/@humanfs/core": { + "version": "0.19.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "../../src/node_modules/@humanfs/node": { + "version": "0.16.6", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "../../src/node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "../../src/node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "../../src/node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "../../src/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "../../src/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "../../src/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "../../src/node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "../../src/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "../../src/node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "../../src/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "../../src/node_modules/@types/eslint": { + "version": "9.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "../../src/node_modules/@types/eslint__js": { + "version": "8.42.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*" + } + }, + "../../src/node_modules/@types/estree": { + "version": "1.0.6", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/@types/node": { + "version": "22.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "../../src/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/type-utils": "8.15.0", + "@typescript-eslint/utils": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../../src/node_modules/@typescript-eslint/parser": { + "version": "8.15.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../../src/node_modules/@typescript-eslint/scope-manager": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "../../src/node_modules/@typescript-eslint/type-utils": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/utils": "8.15.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../../src/node_modules/@typescript-eslint/types": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "../../src/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.15.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../../src/node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../src/node_modules/@typescript-eslint/utils": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../../src/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.15.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "../../src/node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../src/node_modules/acorn": { + "version": "8.14.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "../../src/node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "../../src/node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "../../src/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "../../src/node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "../../src/node_modules/as-console": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "as-rainbow": "^0.1.0", + "table-as": "^1.0.1" + } + }, + "../../src/node_modules/as-rainbow": { + "version": "0.1.0", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/as-test": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "as-console": "^7.0.0", + "as-rainbow": "^0.1.0", + "as-variant": "^0.4.1", + "chalk": "^5.3.0", + "glob": "^11.0.0", + "json-as": "^0.9.21", + "typer-diff": "^1.1.1" + }, + "bin": { + "as-test": "bin/index.js", + "ast": "bin/index.js" + } + }, + "../../src/node_modules/as-test/node_modules/glob": { + "version": "11.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../src/node_modules/as-test/node_modules/minimatch": { + "version": "10.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../src/node_modules/as-variant": { + "version": "0.4.1", + "dev": true + }, + "../../src/node_modules/as-virtual": { + "version": "0.2.0", + "license": "MIT" + }, + "../../src/node_modules/assemblyscript": { + "version": "0.27.31", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "binaryen": "116.0.0-nightly.20240114", + "long": "^5.2.1" + }, + "bin": { + "asc": "bin/asc.js", + "asinit": "bin/asinit.js" + }, + "engines": { + "node": ">=16", + "npm": ">=7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/assemblyscript" + } + }, + "../../src/node_modules/assemblyscript-prettier": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "assemblyscript": "~0.27.0" + }, + "peerDependencies": { + "prettier": "^3.0.0" + } + }, + "../../src/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/binaryen": { + "version": "116.0.0-nightly.20240114", + "dev": true, + "license": "Apache-2.0", + "bin": { + "wasm-opt": "bin/wasm-opt", + "wasm2js": "bin/wasm2js" + } + }, + "../../src/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "../../src/node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../../src/node_modules/chalk": { + "version": "5.3.0", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "../../src/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "../../src/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "../../src/node_modules/debug": { + "version": "4.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "../../src/node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/eslint": { + "version": "9.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.15.0", + "@eslint/plugin-kit": "^0.2.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.5", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "../../src/node_modules/eslint-scope": { + "version": "8.2.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../src/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../src/node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "../../src/node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../src/node_modules/espree": { + "version": "10.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../src/node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../src/node_modules/esquery": { + "version": "1.5.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "../../src/node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "../../src/node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "../../src/node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "../../src/node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/fast-glob": { + "version": "3.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "../../src/node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "../../src/node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/fastq": { + "version": "1.17.1", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "../../src/node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "../../src/node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "../../src/node_modules/flatted": { + "version": "3.3.1", + "dev": true, + "license": "ISC" + }, + "../../src/node_modules/foreground-child": { + "version": "3.2.1", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../src/node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "../../src/node_modules/globals": { + "version": "14.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/ignore": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "../../src/node_modules/import-fresh": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "../../src/node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../../src/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "../../src/node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "../../src/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "../../src/node_modules/jackspeak": { + "version": "4.0.1", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "../../src/node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "../../src/node_modules/json-as": { + "version": "0.9.26", + "license": "MIT", + "dependencies": { + "as-virtual": "^0.2.0" + } + }, + "../../src/node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "../../src/node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "../../src/node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/lodash.clonedeep": { + "version": "4.5.0", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/long": { + "version": "5.2.3", + "dev": true, + "license": "Apache-2.0" + }, + "../../src/node_modules/lru-cache": { + "version": "11.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "../../src/node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "../../src/node_modules/micromatch": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "../../src/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "../../src/node_modules/minimatch/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "../../src/node_modules/minipass": { + "version": "7.1.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "../../src/node_modules/ms": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "../../src/node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/package-json-from-dist": { + "version": "1.0.0", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "../../src/node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "../../src/node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/path-scurry": { + "version": "2.0.0", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../src/node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "../../src/node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "../../src/node_modules/prettier": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "../../src/node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../../src/node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "../../src/node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "../../src/node_modules/reusify": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "../../src/node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "../../src/node_modules/semver": { + "version": "7.6.3", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "../../src/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../src/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "../../src/node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "../../src/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/table-as": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "../../src/node_modules/ts-api-utils": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "../../src/node_modules/ts-mixer": { + "version": "6.0.4", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "../../src/node_modules/typer-diff": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "typescript": "^5.5.2" + } + }, + "../../src/node_modules/typescript": { + "version": "5.6.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "../../src/node_modules/typescript-eslint": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.15.0", + "@typescript-eslint/parser": "8.15.0", + "@typescript-eslint/utils": "8.15.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../../src/node_modules/undici-types": { + "version": "6.19.8", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "../../src/node_modules/visitor-as": { + "version": "0.11.4", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "ts-mixer": "^6.0.2" + }, + "peerDependencies": { + "assemblyscript": "^0.25.0" + } + }, + "../../src/node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "../../src/node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../../src/node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "../../src/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "../../src/node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "../../src/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "../../src/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "../../src/node_modules/xid-ts": { + "version": "1.1.4", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "../../src/node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/core": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", + "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.10.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@hypermode/modus-sdk-as": { + "resolved": "../../src", + "link": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint__js": { + "version": "8.42.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.21.0.tgz", + "integrity": "sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/type-utils": "8.21.0", + "@typescript-eslint/utils": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.21.0.tgz", + "integrity": "sha512-Wy+/sdEH9kI3w9civgACwabHbKl+qIOu0uFZ9IMKzX3Jpv9og0ZBJrZExGrPpFAY7rWsXuxs5e7CPPP17A4eYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/typescript-estree": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.21.0.tgz", + "integrity": "sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.21.0.tgz", + "integrity": "sha512-95OsL6J2BtzoBxHicoXHxgk3z+9P3BEcQTpBKriqiYzLKnM2DeSqs+sndMKdamU8FosiadQFT3D+BSL9EKnAJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.21.0", + "@typescript-eslint/utils": "8.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.21.0.tgz", + "integrity": "sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.21.0.tgz", + "integrity": "sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.21.0.tgz", + "integrity": "sha512-xcXBfcq0Kaxgj7dwejMbFyq7IOHgpNMtVuDveK7w3ZGwG9owKzhALVwKpTF2yrZmEwl9SWdetf3fxNzJQaVuxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/typescript-estree": "8.21.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.21.0.tgz", + "integrity": "sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.21.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/as-virtual": { + "version": "0.2.0", + "license": "MIT" + }, + "node_modules/assemblyscript": { + "version": "0.27.32", + "resolved": "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.27.32.tgz", + "integrity": "sha512-A8ULHwC6hp4F3moAeQWdciKoccZGZLM9Fsk4pQGywY/fd/S+tslmqBcINroFSI9tW18nO9mC8zh76bik1BkyUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "binaryen": "116.0.0-nightly.20240114", + "long": "^5.2.1" + }, + "bin": { + "asc": "bin/asc.js", + "asinit": "bin/asinit.js" + }, + "engines": { + "node": ">=16", + "npm": ">=7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/assemblyscript" + } + }, + "node_modules/assemblyscript-prettier": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "assemblyscript": "~0.27.0" + }, + "peerDependencies": { + "prettier": "^3.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/binaryen": { + "version": "116.0.0-nightly.20240114", + "dev": true, + "license": "Apache-2.0", + "bin": { + "wasm-opt": "bin/wasm-opt", + "wasm2js": "bin/wasm2js" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz", + "integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.10.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.18.0", + "@eslint/plugin-kit": "^0.2.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "dev": true, + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-as": { + "version": "0.9.28", + "resolved": "https://registry.npmjs.org/json-as/-/json-as-0.9.28.tgz", + "integrity": "sha512-IsjpsoUix0nXwW9A5iU15EFLuB62JEiZTIytsnPrfoe3qkJVUTiHWctjoYNAbysMgLRiBWDdTepu/ENnx5LbOg==", + "license": "MIT", + "dependencies": { + "as-virtual": "^0.2.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.2.3", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.21.0.tgz", + "integrity": "sha512-txEKYY4XMKwPXxNkN8+AxAdX6iIJAPiJbHE/FpQccs/sxw8Lf26kqwC3cn0xkHlW8kEbLhkhCsjWuMveaY9Rxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.21.0", + "@typescript-eslint/parser": "8.21.0", + "@typescript-eslint/utils": "8.21.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/visitor-as": { + "version": "0.11.4", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "ts-mixer": "^6.0.2" + }, + "peerDependencies": { + "assemblyscript": "^0.25.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/sdk/assemblyscript/examples/mysql/package.json b/sdk/assemblyscript/examples/mysql/package.json new file mode 100644 index 000000000..77c2bfba0 --- /dev/null +++ b/sdk/assemblyscript/examples/mysql/package.json @@ -0,0 +1,33 @@ +{ + "name": "mysql-example", + "private": true, + "description": "Modus AssemblyScript Example using MySQL database", + "author": "Hypermode Inc.", + "license": "Apache-2.0", + "type": "module", + "scripts": { + "build": "modus-as-build", + "lint": "eslint .", + "pretty": "prettier --write .", + "pretty:check": "prettier --check ." + }, + "dependencies": { + "@hypermode/modus-sdk-as": "../../src", + "json-as": "^0.9.28" + }, + "devDependencies": { + "@eslint/js": "^9.18.0", + "@types/eslint__js": "^8.42.3", + "@typescript-eslint/parser": "^8.21.0", + "assemblyscript": "^0.27.32", + "assemblyscript-prettier": "^3.0.1", + "eslint": "^9.18.0", + "prettier": "^3.4.2", + "typescript": "^5.7.3", + "typescript-eslint": "^8.21.0", + "visitor-as": "^0.11.4" + }, + "overrides": { + "assemblyscript": "$assemblyscript" + } +} diff --git a/sdk/assemblyscript/src/assembly/database.ts b/sdk/assemblyscript/src/assembly/database.ts index 3239e6a7a..0812c8a97 100644 --- a/sdk/assemblyscript/src/assembly/database.ts +++ b/sdk/assemblyscript/src/assembly/database.ts @@ -23,6 +23,7 @@ class HostQueryResponse { error!: string | null; resultJson!: string | null; rowsAffected!: u32; + lastInsertId!: u64; } interface Params { @@ -66,6 +67,7 @@ export class NamedParams implements Params { export class Response { error: string | null = null; rowsAffected: u32 = 0; + lastInsertId: u64 = 0; } export class QueryResponse extends Response { @@ -82,7 +84,11 @@ export function execute( statement: string, params: Params, ): Response { - const paramsJson = params.toJSON(); + let paramsJson = params.toJSON(); + + // This flag instructs the host function not to return rows, but to simply execute the statement. + paramsJson = "exec:" + paramsJson; + const response = hostExecuteQuery( hostName, dbType, @@ -101,6 +107,7 @@ export function execute( const results: Response = { error: response.error, rowsAffected: response.rowsAffected, + lastInsertId: response.lastInsertId, }; return results; @@ -132,6 +139,7 @@ export function query( error: response.error, rows: response.resultJson ? JSON.parse(response.resultJson!) : [], rowsAffected: response.rowsAffected, + lastInsertId: response.rowsAffected, }; return results; @@ -153,5 +161,155 @@ export function queryScalar( error: response.error, value: response.rows[0].values()[0], rowsAffected: response.rowsAffected, + lastInsertId: response.rowsAffected, }; } + +/** + * Represents a point in 2D space, having `x` and `y` coordinates. + * Correctly serializes to and from a SQL point type, in (x, y) order. + * + * Note that this class is identical to the Location class, but uses different field names. + */ +export class Point { + constructor( + public x: f64, + public y: f64, + ) {} + + public toString(): string { + return `(${this.x},${this.y})`; + } + + public static fromString(data: string): Point | null { + const p = parsePointString(data); + if (p.length == 0) { + return null; + } + return new Point(p[0], p[1]); + } + + // The following methods are required for custom JSON serialization + // This is used in lieu of the @json decorator, so that the class can be + // serialized to a string in SQL format. + + __INITIALIZE(): this { + return this; + } + + __SERIALIZE(): string { + return this.toString(); + } + + /* eslint-disable @typescript-eslint/no-unused-vars */ + __DESERIALIZE( + data: string, + key_start: i32, + key_end: i32, + value_start: i32, + value_end: i32, + ): boolean { + if ( + data.length < 7 || + data.charAt(0) != '"' || + data.charAt(data.length - 1) != '"' + ) { + return false; + } + + const p = parsePointString(data.substring(1, data.length - 1)); + if (p.length == 0) { + return false; + } + + this.x = p[0]; + this.y = p[1]; + return true; + } +} + +/** + * Represents a location on Earth, having `longitude` and `latitude` coordinates. + * Correctly serializes to and from a SQL point type, in (longitude, latitude) order. + * + * Note that this class is identical to the `Point` class, but uses different field names. + */ +export class Location { + constructor( + public longitude: f64, + public latitude: f64, + ) {} + + public toString(): string { + return `(${this.longitude},${this.latitude})`; + } + + public static fromString(data: string): Point | null { + const p = parsePointString(data); + if (p.length == 0) { + return null; + } + return new Point(p[0], p[1]); + } + + // The following methods are required for custom JSON serialization + // This is used in lieu of the @json decorator, so that the class can be + // serialized to a string in SQL format. + + __INITIALIZE(): this { + return this; + } + + __SERIALIZE(): string { + return this.toString(); + } + + /* eslint-disable @typescript-eslint/no-unused-vars */ + __DESERIALIZE( + data: string, + key_start: i32, + key_end: i32, + value_start: i32, + value_end: i32, + ): boolean { + if ( + data.length < 7 || + data.charAt(0) != '"' || + data.charAt(data.length - 1) != '"' + ) { + return false; + } + + const p = parsePointString(data.substring(1, data.length - 1)); + if (p.length == 0) { + return false; + } + + this.longitude = p[0]; + this.latitude = p[1]; + return true; + } +} + +function parsePointString(data: string): f64[] { + // Convert WKT point to Postgres format + // "POINT (x y)" -> "(x, y)" + if (data.startsWith("POINT (") && data.endsWith(")")) { + data = data.substring(6, data.length).replace(" ", ","); + } + + if (!data.startsWith("(") || !data.endsWith(")")) { + console.error(`Invalid Point string: "${data}"`); + return []; + } + + const parts = data.substring(1, data.length - 1).split(","); + if (parts.length != 2) { + console.error(`Invalid Point string: "${data}"`); + return []; + } + + const x = parseFloat(parts[0].trim()); + const y = parseFloat(parts[1].trim()); + return [x, y]; +} diff --git a/sdk/assemblyscript/src/assembly/index.ts b/sdk/assemblyscript/src/assembly/index.ts index 8f83d1f38..418892163 100644 --- a/sdk/assemblyscript/src/assembly/index.ts +++ b/sdk/assemblyscript/src/assembly/index.ts @@ -10,6 +10,9 @@ import * as postgresql from "./postgresql"; export { postgresql }; +import * as mysql from "./mysql"; +export { mysql }; + import * as dgraph from "./dgraph"; export { dgraph }; diff --git a/sdk/assemblyscript/src/assembly/mysql.ts b/sdk/assemblyscript/src/assembly/mysql.ts new file mode 100644 index 000000000..d7c58af29 --- /dev/null +++ b/sdk/assemblyscript/src/assembly/mysql.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2025 Hypermode Inc. + * Licensed under the terms of the Apache License, Version 2.0 + * See the LICENSE file that accompanied this code for further details. + * + * SPDX-FileCopyrightText: 2025 Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as db from "./database"; +import { + PositionalParams as Params, + Response, + QueryResponse, + ScalarResponse, + Point, + Location, +} from "./database"; + +export { Params, Response, QueryResponse, ScalarResponse, Point, Location }; + +const dbType = "mysql"; + +export function execute( + hostName: string, + statement: string, + params: Params = new Params(), +): Response { + return db.execute(hostName, dbType, statement, params); +} + +export function query( + hostName: string, + statement: string, + params: Params = new Params(), +): QueryResponse { + return db.query(hostName, dbType, statement, params); +} + +export function queryScalar( + hostName: string, + statement: string, + params: Params = new Params(), +): ScalarResponse { + return db.queryScalar(hostName, dbType, statement, params); +} diff --git a/sdk/assemblyscript/src/assembly/postgresql.ts b/sdk/assemblyscript/src/assembly/postgresql.ts index 6a9bdd628..d582959d2 100644 --- a/sdk/assemblyscript/src/assembly/postgresql.ts +++ b/sdk/assemblyscript/src/assembly/postgresql.ts @@ -13,9 +13,11 @@ import { Response, QueryResponse, ScalarResponse, + Point, + Location, } from "./database"; -export { Params, Response, QueryResponse, ScalarResponse }; +export { Params, Response, QueryResponse, ScalarResponse, Point, Location }; const dbType = "postgresql"; @@ -42,146 +44,3 @@ export function queryScalar( ): ScalarResponse { return db.queryScalar(hostName, dbType, statement, params); } - -function parsePointString(data: string): f64[] { - if (!data.startsWith("(") || !data.endsWith(")")) { - console.error(`Invalid Point string: "${data}"`); - return []; - } - - const parts = data.substring(1, data.length - 1).split(","); - if (parts.length != 2) { - console.error(`Invalid Point string: "${data}"`); - return []; - } - - const x = parseFloat(parts[0].trim()); - const y = parseFloat(parts[1].trim()); - return [x, y]; -} - -/** - * Represents a point in 2D space, having `x` and `y` coordinates. - * Correctly serializes to and from PostgreSQL's point type, in (x, y) order. - * - * Note that this class is identical to the Location class, but uses different field names. - */ -export class Point { - constructor( - public x: f64, - public y: f64, - ) {} - - public toString(): string { - return `(${this.x},${this.y})`; - } - - public static fromString(data: string): Point | null { - const p = parsePointString(data); - if (p.length == 0) { - return null; - } - return new Point(p[0], p[1]); - } - - // The following methods are required for custom JSON serialization - // This is used in lieu of the @json decorator, so that the class can be - // serialized to a string in PostgreSQL format. - - __INITIALIZE(): this { - return this; - } - - __SERIALIZE(): string { - return this.toString(); - } - - /* eslint-disable @typescript-eslint/no-unused-vars */ - __DESERIALIZE( - data: string, - key_start: i32, - key_end: i32, - value_start: i32, - value_end: i32, - ): boolean { - if ( - data.length < 7 || - data.charAt(0) != '"' || - data.charAt(data.length - 1) != '"' - ) { - return false; - } - - const p = parsePointString(data.substring(1, data.length - 1)); - if (p.length == 0) { - return false; - } - - this.x = p[0]; - this.y = p[1]; - return true; - } -} - -/** - * Represents a location on Earth, having `longitude` and `latitude` coordinates. - * Correctly serializes to and from PostgreSQL's point type, in (longitude, latitude) order. - * - * Note that this class is identical to the `Point` class, but uses different field names. - */ -export class Location { - constructor( - public longitude: f64, - public latitude: f64, - ) {} - - public toString(): string { - return `(${this.longitude},${this.latitude})`; - } - - public static fromString(data: string): Point | null { - const p = parsePointString(data); - if (p.length == 0) { - return null; - } - return new Point(p[0], p[1]); - } - - // The following methods are required for custom JSON serialization - // This is used in lieu of the @json decorator, so that the class can be - // serialized to a string in PostgreSQL format. - - __INITIALIZE(): this { - return this; - } - - __SERIALIZE(): string { - return this.toString(); - } - - /* eslint-disable @typescript-eslint/no-unused-vars */ - __DESERIALIZE( - data: string, - key_start: i32, - key_end: i32, - value_start: i32, - value_end: i32, - ): boolean { - if ( - data.length < 7 || - data.charAt(0) != '"' || - data.charAt(data.length - 1) != '"' - ) { - return false; - } - - const p = parsePointString(data.substring(1, data.length - 1)); - if (p.length == 0) { - return false; - } - - this.longitude = p[0]; - this.latitude = p[1]; - return true; - } -} diff --git a/sdk/go/examples/mysql/build.cmd b/sdk/go/examples/mysql/build.cmd new file mode 100644 index 000000000..ed9d2952f --- /dev/null +++ b/sdk/go/examples/mysql/build.cmd @@ -0,0 +1,12 @@ +@echo off + +:: This build script works best for examples that are in this repository. +:: If you are using this as a template for your own project, you may need to modify this script, +:: to invoke the modus-go-build tool with the correct path to your project. + +SET "PROJECTDIR=%~dp0" +pushd ..\..\tools\modus-go-build > nul +go run . "%PROJECTDIR%" +set "exit_code=%ERRORLEVEL%" +popd > nul +exit /b %exit_code% diff --git a/sdk/go/examples/mysql/build.sh b/sdk/go/examples/mysql/build.sh new file mode 100755 index 000000000..93bc3e15b --- /dev/null +++ b/sdk/go/examples/mysql/build.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# This build script works best for examples that are in this repository. +# If you are using this as a template for your own project, you may need to modify this script, +# to invoke the modus-go-build tool with the correct path to your project. + +PROJECTDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +pushd ../../tools/modus-go-build >/dev/null || exit +go run . "${PROJECTDIR}" +exit_code=$? +popd >/dev/null || exit +exit "${exit_code}" diff --git a/sdk/go/examples/mysql/go.mod b/sdk/go/examples/mysql/go.mod new file mode 100644 index 000000000..2a25df618 --- /dev/null +++ b/sdk/go/examples/mysql/go.mod @@ -0,0 +1,7 @@ +module mysql-example + +go 1.23.1 + +toolchain go1.23.5 + +require github.com/hypermodeinc/modus/sdk/go v0.16.0 diff --git a/sdk/go/examples/mysql/go.sum b/sdk/go/examples/mysql/go.sum new file mode 100644 index 000000000..cef3cd84e --- /dev/null +++ b/sdk/go/examples/mysql/go.sum @@ -0,0 +1,2 @@ +github.com/hypermodeinc/modus/sdk/go v0.16.0 h1:kO43jI1RfDgDJAEg+ilNSJIUHfsSXM2pGuHh/dT0Zec= +github.com/hypermodeinc/modus/sdk/go v0.16.0/go.mod h1:wXGlRRd/Rkxv7QwYfHzc2VoaU91lzWpiZ/zm4sTuD/U= diff --git a/sdk/go/examples/mysql/main.go b/sdk/go/examples/mysql/main.go new file mode 100644 index 000000000..8171581fb --- /dev/null +++ b/sdk/go/examples/mysql/main.go @@ -0,0 +1,91 @@ +/* + * This example is part of the Modus project, licensed under the Apache License 2.0. + * You may modify and use this example in accordance with the license. + * See the LICENSE file that accompanied this code for further details. + */ + +package main + +import ( + "fmt" + + "github.com/hypermodeinc/modus/sdk/go/pkg/mysql" +) + +// The name of the MySQL host, as specified in the modus.json manifest +const host = "my-database" + +type Person struct { + Id int `json:"id"` + Name string `json:"name"` + Age int `json:"age"` + Home *mysql.Location `json:"home"` +} + +func GetAllPeople() ([]Person, error) { + const query = "select * from people order by id" + response, err := mysql.Query[Person](host, query) + return response.Rows, err +} + +func GetPeopleByName(name string) ([]Person, error) { + const query = "select * from people where name = ?" + response, err := mysql.Query[Person](host, query, name) + return response.Rows, err +} + +func GetPerson(id int) (*Person, error) { + const query = "select * from people where id = ?" + response, err := mysql.Query[Person](host, query, id) + if err != nil { + return nil, err + } + + if len(response.Rows) == 0 { + return nil, nil // Person not found + } + + return &response.Rows[0], nil +} + +func AddPerson(name string, age int) (*Person, error) { + const query = "insert into people (name, age) values (?, ?)" + + response, err := mysql.Execute(host, query, name, age) + if err != nil { + return nil, fmt.Errorf("Failed to add person to database: %v", err) + } + + p := Person{Id: int(response.LastInsertId), Name: name, Age: age} + return &p, nil +} + +func UpdatePersonHome(id int, longitude, latitude float64) (*Person, error) { + const query = "update people set home = point(?,?) where id = ?" + + response, err := mysql.Execute(host, query, longitude, latitude, id) + if err != nil { + return nil, fmt.Errorf("Failed to update person in database: %v", err) + } + + if response.RowsAffected != 1 { + return nil, fmt.Errorf("Failed to update person with id %d. The record may not exist.", id) + } + + return GetPerson(id) +} + +func DeletePerson(id int) (string, error) { + const query = "delete from people where id = ?" + + response, err := mysql.Execute(host, query, id) + if err != nil { + return "", fmt.Errorf("Failed to delete person from database: %v", err) + } + + if response.RowsAffected != 1 { + return "", fmt.Errorf("Failed to delete person with id %d. The record may not exist.", id) + } + + return "success", nil +} diff --git a/sdk/go/examples/mysql/modus.json b/sdk/go/examples/mysql/modus.json new file mode 100644 index 000000000..064a4d04a --- /dev/null +++ b/sdk/go/examples/mysql/modus.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://schema.hypermode.com/modus.json", + "endpoints": { + "default": { + "type": "graphql", + "path": "/graphql", + "auth": "bearer-token" + } + }, + "connections": { + // This example shows how you can set a host that references a MySQL database. + // The connection string should be set using URI format. See the MySQL documentation for more information: + // https://dev.mysql.com/doc/refman/8.4/en/connecting-using-uri-or-key-value-pairs.html#connecting-using-uri + // + // However, any optional parameters provided should be in the form specified here: + // https://github.com/go-sql-driver/mysql/blob/master/README.md#parameters + // + // For example, use tls=true to enable encryption (not sslmode=require) + // + // Where secrets are required, {{SECRET_NAME}} templates are replaced with the values + // specified in the Hypermode Console. Do not include actual secret values in this file. + + "my-database": { + "type": "mysql", + "connString": "mysql://{{USERNAME}}:{{PASSWORD}}@database.example.com:3306/dbname?tls=true" + + // For testing with a local db instance, replace the above line with the following: + // "connString": "mysql://root@localhost/mydb" + } + } +} diff --git a/sdk/go/examples/postgresql/main.go b/sdk/go/examples/postgresql/main.go index 21cd6f25d..fd85a1ec5 100644 --- a/sdk/go/examples/postgresql/main.go +++ b/sdk/go/examples/postgresql/main.go @@ -24,51 +24,51 @@ type Person struct { func GetAllPeople() ([]Person, error) { const query = "select * from people order by id" - rows, _, err := postgresql.Query[Person](host, query) - return rows, err + response, err := postgresql.Query[Person](host, query) + return response.Rows, err } func GetPeopleByName(name string) ([]Person, error) { const query = "select * from people where name = $1" - rows, _, err := postgresql.Query[Person](host, query, name) - return rows, err + response, err := postgresql.Query[Person](host, query, name) + return response.Rows, err } func GetPerson(id int) (*Person, error) { const query = "select * from people where id = $1" - rows, _, err := postgresql.Query[Person](host, query, id) + response, err := postgresql.Query[Person](host, query, id) if err != nil { return nil, err } - if len(rows) == 0 { + if len(response.Rows) == 0 { return nil, nil // Person not found } - return &rows[0], nil + return &response.Rows[0], nil } func AddPerson(name string, age int) (*Person, error) { const query = "insert into people (name, age) values ($1, $2) RETURNING id" - id, _, err := postgresql.QueryScalar[int](host, query, name, age) + response, err := postgresql.QueryScalar[int](host, query, name, age) if err != nil { return nil, fmt.Errorf("Failed to add person to database: %v", err) } - p := Person{Id: id, Name: name, Age: age} + p := Person{Id: response.Value, Name: name, Age: age} return &p, nil } func UpdatePersonHome(id int, longitude, latitude float64) (*Person, error) { const query = "update people set home = point($1, $2) where id = $3" - affected, err := postgresql.Execute(host, query, longitude, latitude, id) + response, err := postgresql.Execute(host, query, longitude, latitude, id) if err != nil { return nil, fmt.Errorf("Failed to update person in database: %v", err) } - if affected != 1 { + if response.RowsAffected != 1 { return nil, fmt.Errorf("Failed to update person with id %d. The record may not exist.", id) } @@ -78,12 +78,12 @@ func UpdatePersonHome(id int, longitude, latitude float64) (*Person, error) { func DeletePerson(id int) (string, error) { const query = "delete from people where id = $1" - affected, err := postgresql.Execute(host, query, id) + response, err := postgresql.Execute(host, query, id) if err != nil { return "", fmt.Errorf("Failed to delete person from database: %v", err) } - if affected != 1 { + if response.RowsAffected != 1 { return "", fmt.Errorf("Failed to delete person with id %d. The record may not exist.", id) } diff --git a/sdk/go/pkg/db/db.go b/sdk/go/pkg/db/db.go index 61fbfefb5..ad828d7fc 100644 --- a/sdk/go/pkg/db/db.go +++ b/sdk/go/pkg/db/db.go @@ -21,78 +21,141 @@ type HostQueryResponse struct { Error *string ResultJson *string RowsAffected uint32 + LastInsertId uint64 } -func Execute(hostName, dbType, statement string, params ...any) (uint, error) { - _, affected, err := doQuery(hostName, dbType, statement, params...) - return affected, err +// Represents the result of a database query that does not return data. +type Response struct { + + // The number of rows affected by the query. + RowsAffected uint32 + + // The ID of the last row inserted by the query, if applicable. + // Note that not all databases support this feature. + LastInsertId uint64 +} + +// Represents the result of a database query that returns rows. +type QueryResponse[T any] struct { + + // The number of rows affected by the query, which typically corresponds to the number of rows returned. + RowsAffected uint32 + + // The ID of the last row inserted by the query, if applicable. + // Note that not all databases support this feature. + LastInsertId uint64 + + // The rows returned by the query. + Rows []T +} + +// Represents the result of a database query that returns a single scalar value. +type ScalarResponse[T any] struct { + + // The number of rows affected by the query, which is typically 1 unless the query failed, + // or had side effects that modified more than one row. + RowsAffected uint32 + + // The ID of the last row inserted by the query, if applicable. + // Note that not all databases support this feature. + LastInsertId uint64 + + // The scalar value returned by the query. + Value T +} + +// Executes a database query that does not return rows. +func Execute(hostName, dbType, statement string, params ...any) (*Response, error) { + r, err := doQuery(hostName, dbType, statement, params, true) + if err != nil { + return nil, err + } + + return &Response{ + RowsAffected: r.RowsAffected, + LastInsertId: r.LastInsertId, + }, nil } -func Query[T any](hostName, dbType, statement string, params ...any) ([]T, uint, error) { - resultJson, affected, err := doQuery(hostName, dbType, statement, params...) +// Executes a database query that returns rows. +// The structure of the rows is determined by the type parameter. +func Query[T any](hostName, dbType, statement string, params ...any) (*QueryResponse[T], error) { + r, err := doQuery(hostName, dbType, statement, params, false) if err != nil { - return nil, affected, err + return nil, err } var rows []T - if resultJson != nil { - if err := utils.JsonDeserialize([]byte(*resultJson), &rows); err != nil { - return nil, affected, fmt.Errorf("could not JSON deserialize database response: %v", err) + if r.ResultJson != nil { + if err := utils.JsonDeserialize([]byte(*r.ResultJson), &rows); err != nil { + return nil, fmt.Errorf("could not JSON deserialize database response: %v", err) } } - return rows, affected, nil + return &QueryResponse[T]{ + RowsAffected: r.RowsAffected, + LastInsertId: r.LastInsertId, + Rows: rows, + }, nil } -func QueryScalar[T any](hostName, dbType, statement string, params ...any) (T, uint, error) { - var zero T - - rows, affected, err := Query[map[string]any](hostName, dbType, statement, params...) +// Executes a database query that returns a single scalar value. +// The type parameter determines the type of the scalar value. +func QueryScalar[T any](hostName, dbType, statement string, params ...any) (*ScalarResponse[T], error) { + r, err := Query[map[string]any](hostName, dbType, statement, params...) if err != nil { - return zero, affected, err + return nil, err } - if len(rows) == 1 { - fields := rows[0] + if len(r.Rows) == 1 { + fields := r.Rows[0] if len(fields) > 1 { - return zero, affected, fmt.Errorf("expected a single column from a scalar database query, but received %d", len(fields)) + return nil, fmt.Errorf("expected a single column from a scalar database query, but received %d", len(fields)) } for _, value := range fields { result, err := utils.ConvertInterfaceTo[T](value) if err != nil { - return zero, affected, fmt.Errorf("could not convert database result to %T: %v", zero, err) + var zero T + return nil, fmt.Errorf("could not convert database result to %T: %v", zero, err) } - return result, affected, nil + return &ScalarResponse[T]{ + RowsAffected: r.RowsAffected, + LastInsertId: r.LastInsertId, + Value: result, + }, nil } - } else if len(rows) > 1 { - return zero, affected, fmt.Errorf("expected a single row from a scalar database query, but received %d", len(rows)) + } else if len(r.Rows) > 1 { + return nil, fmt.Errorf("expected a single row from a scalar database query, but received %d", len(r.Rows)) } - return zero, affected, errors.New("no result returned from database query") + return nil, errors.New("no result returned from database query") } -func doQuery(hostName, dbType, statement string, params ...any) (*string, uint, error) { +func doQuery(hostName, dbType, statement string, params []any, execOnly bool) (*HostQueryResponse, error) { paramsJson := "[]" if len(params) > 0 { bytes, err := utils.JsonSerialize(params) if err != nil { - return nil, 0, fmt.Errorf("could not JSON serialize query parameters: %v", err) + return nil, fmt.Errorf("could not JSON serialize query parameters: %v", err) } paramsJson = string(bytes) } + if execOnly { + // This flag instructs the host function not to return rows, but to simply execute the statement. + paramsJson = "exec:" + paramsJson + } + statement = strings.TrimSpace(statement) response := hostExecuteQuery(&hostName, &dbType, &statement, ¶msJson) if response == nil { - return nil, 0, errors.New("no response received from database query") + return nil, errors.New("no response received from database query") } - affected := uint(response.RowsAffected) - if response.Error != nil { - return nil, affected, fmt.Errorf("database returned an error: %s", *response.Error) + return nil, fmt.Errorf("database returned an error: %s", *response.Error) } - return response.ResultJson, affected, nil + return response, nil } diff --git a/sdk/go/pkg/db/db_test.go b/sdk/go/pkg/db/db_test.go index 744275376..18829d264 100644 --- a/sdk/go/pkg/db/db_test.go +++ b/sdk/go/pkg/db/db_test.go @@ -12,6 +12,7 @@ package db_test import ( + "strings" "testing" "github.com/hypermodeinc/modus/sdk/go/pkg/db" @@ -24,30 +25,30 @@ var ( ) func TestExecute(t *testing.T) { - affected, err := db.Execute(testHostName, testDbType, db.MockExecuteStatement, db.MockExecuteParameters...) + r, err := db.Execute(testHostName, testDbType, db.MockExecuteStatement, db.MockExecuteParameters...) if err != nil { t.Fatalf("Expected no error, but received: %s", err) } - if affected != 3 { - t.Errorf("Expected 3 rows affected, but received: %d", affected) + if r.RowsAffected != 3 { + t.Errorf("Expected 3 rows affected, but received: %d", r.RowsAffected) } - testCallStack(t, db.MockExecuteStatement, db.MockExecuteParameters) + testCallStack(t, db.MockExecuteStatement, db.MockExecuteParameters, "exec") } func TestQuery(t *testing.T) { - rows, affected, err := db.Query[map[string]any](testHostName, testDbType, db.MockQueryStatement, db.MockQueryParameters...) + r, err := db.Query[map[string]any](testHostName, testDbType, db.MockQueryStatement, db.MockQueryParameters...) if err != nil { t.Fatalf("Expected no error, but received: %s", err) } - if affected != 3 { - t.Errorf("Expected 3 rows affected, but received: %d", affected) + if r.RowsAffected != 3 { + t.Errorf("Expected 3 rows affected, but received: %d", r.RowsAffected) } - if len(rows) != 3 { - t.Errorf("Expected 3 rows, but received: %d", len(rows)) + if len(r.Rows) != 3 { + t.Errorf("Expected 3 rows, but received: %d", len(r.Rows)) } - for i, row := range rows { + for i, row := range r.Rows { n, err := utils.ConvertInterfaceTo[int](row["id"]) if err != nil { t.Fatalf("Expected no error, but received: %s", err) @@ -64,22 +65,22 @@ func TestQuery(t *testing.T) { } func TestQueryScalar(t *testing.T) { - result, affected, err := db.QueryScalar[int](testHostName, testDbType, db.MockQueryScalarStatement, db.MockQueryScalarParameters...) + r, err := db.QueryScalar[int](testHostName, testDbType, db.MockQueryScalarStatement, db.MockQueryScalarParameters...) if err != nil { t.Fatalf("Expected no error, but received: %s", err) } - if affected != 1 { - t.Errorf("Expected 1 rows affected, but received: %d", affected) + if r.RowsAffected != 1 { + t.Errorf("Expected 1 rows affected, but received: %d", r.RowsAffected) } - if result != 3 { - t.Errorf("Expected result: 3, but received: %d", result) + if r.Value != 3 { + t.Errorf("Expected result: 3, but received: %d", r.Value) } testCallStack(t, db.MockQueryScalarStatement, db.MockQueryScalarParameters) } -func testCallStack(t *testing.T, expectedStatement string, expectedParams []any) { +func testCallStack(t *testing.T, expectedStatement string, expectedParams []any, flags ...string) { values := db.DatabaseQueryCallStack.Pop() if values == nil { t.Error("Expected a query, but none was found.") @@ -103,6 +104,9 @@ func testCallStack(t *testing.T, expectedStatement string, expectedParams []any) bytes, _ := utils.JsonSerialize(expectedParams) expectedParamsJson := string(bytes) + if len(flags) > 0 { + expectedParamsJson = strings.Join(flags, ",") + ":" + expectedParamsJson + } if *receivedJson != expectedParamsJson { t.Errorf("Expected paramsJson: \"%s\", but received: \"%s\"", expectedParamsJson, *receivedJson) } diff --git a/sdk/go/pkg/postgresql/location.go b/sdk/go/pkg/db/location.go similarity index 58% rename from sdk/go/pkg/postgresql/location.go rename to sdk/go/pkg/db/location.go index 67d8f5bb8..0f4ee4a2c 100644 --- a/sdk/go/pkg/postgresql/location.go +++ b/sdk/go/pkg/db/location.go @@ -7,13 +7,24 @@ * SPDX-License-Identifier: Apache-2.0 */ -package postgresql +package db -import "fmt" +import ( + "fmt" + "strings" +) +// Represents a location on Earth, having longitude and latitude coordinates. +// Correctly serializes to and from a SQL point type, in (longitude, latitude) order. +// +// Note that this struct is identical to the Point struct, but uses different field names. type Location struct { + + // The Longitude coordinate of the location. Longitude float64 `json:"longitude"` - Latitude float64 `json:"latitude"` + + // The Latitude coordinate of the location. + Latitude float64 `json:"latitude"` } func (l *Location) String() string { @@ -44,12 +55,19 @@ func (l *Location) UnmarshalJSON(data []byte) error { return nil } +// Creates a new Location with the specified longitude and latitude coordinates. func NewLocation(longitude, latitude float64) *Location { return &Location{longitude, latitude} } +// Parses a location from a string in the format "(Longitude,Latitude)" or "POINT (Longitude Latitude)". func ParseLocation(s string) (*Location, error) { var l Location - _, err := fmt.Sscanf(s, "(%f,%f)", &l.Longitude, &l.Latitude) + var err error + if strings.HasPrefix(s, "POINT (") { + _, err = fmt.Sscanf(s, "POINT (%f %f)", &l.Longitude, &l.Latitude) + } else { + _, err = fmt.Sscanf(s, "(%f,%f)", &l.Longitude, &l.Latitude) + } return &l, err } diff --git a/sdk/go/pkg/postgresql/location_test.go b/sdk/go/pkg/db/location_test.go similarity index 77% rename from sdk/go/pkg/postgresql/location_test.go rename to sdk/go/pkg/db/location_test.go index 779af6edd..0b925e380 100644 --- a/sdk/go/pkg/postgresql/location_test.go +++ b/sdk/go/pkg/db/location_test.go @@ -7,17 +7,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -package postgresql_test +package db_test import ( "encoding/json" "testing" - "github.com/hypermodeinc/modus/sdk/go/pkg/postgresql" + "github.com/hypermodeinc/modus/sdk/go/pkg/db" ) func TestLocationString(t *testing.T) { - location := postgresql.NewLocation(12.345678901234567, -56.7890123456789) + location := db.NewLocation(12.345678901234567, -56.7890123456789) expected := "(12.345678901234567,-56.7890123456789)" result := location.String() @@ -27,7 +27,7 @@ func TestLocationString(t *testing.T) { } func TestLocationMarshalJSON(t *testing.T) { - location := postgresql.NewLocation(12.345678901234567, -56.7890123456789) + location := db.NewLocation(12.345678901234567, -56.7890123456789) expected := `"(12.345678901234567,-56.7890123456789)"` result, err := json.Marshal(location) @@ -42,8 +42,8 @@ func TestLocationMarshalJSON(t *testing.T) { func TestLocationUnmarshalJSON(t *testing.T) { data := []byte(`"(12.345678901234567,-56.7890123456789)"`) - expected := postgresql.NewLocation(12.345678901234567, -56.7890123456789) - location := &postgresql.Location{} + expected := db.NewLocation(12.345678901234567, -56.7890123456789) + location := &db.Location{} err := json.Unmarshal(data, location) @@ -59,7 +59,7 @@ func TestLocationUnmarshalJSON(t *testing.T) { func TestNewLocation(t *testing.T) { longitude := 12.345678901234567 latitude := -56.7890123456789 - location := postgresql.NewLocation(longitude, latitude) + location := db.NewLocation(longitude, latitude) if location.Longitude != longitude || location.Latitude != latitude { t.Errorf("Expected Longitude: %f, Latitude: %f, but got %+v", longitude, latitude, location) @@ -68,8 +68,8 @@ func TestNewLocation(t *testing.T) { func TestParseLocation(t *testing.T) { s := "(12.345678901234567,-56.7890123456789)" - expected := postgresql.NewLocation(12.345678901234567, -56.7890123456789) - location, err := postgresql.ParseLocation(s) + expected := db.NewLocation(12.345678901234567, -56.7890123456789) + location, err := db.ParseLocation(s) if err != nil { t.Errorf("Unexpected error: %v", err) @@ -82,7 +82,7 @@ func TestParseLocation(t *testing.T) { func TestParseLocationError(t *testing.T) { s := "invalid-location" - _, err := postgresql.ParseLocation(s) + _, err := db.ParseLocation(s) if err == nil { t.Error("Expected an error, but received none") diff --git a/sdk/go/pkg/postgresql/point.go b/sdk/go/pkg/db/point.go similarity index 61% rename from sdk/go/pkg/postgresql/point.go rename to sdk/go/pkg/db/point.go index f0699f362..37ab6fec9 100644 --- a/sdk/go/pkg/postgresql/point.go +++ b/sdk/go/pkg/db/point.go @@ -7,12 +7,23 @@ * SPDX-License-Identifier: Apache-2.0 */ -package postgresql +package db -import "fmt" +import ( + "fmt" + "strings" +) +// Represents a point in 2D space, having X and Y coordinates. +// Correctly serializes to and from a SQL point type, in (X, Y) order. +// +// Note that this struct is identical to the Location struct, but uses different field names. type Point struct { + + // The X coordinate of the point. X float64 `json:"x"` + + // The Y coordinate of the point. Y float64 `json:"y"` } @@ -44,12 +55,19 @@ func (p *Point) UnmarshalJSON(data []byte) error { return nil } +// Creates a new Point with the specified X and Y coordinates. func NewPoint(x, y float64) *Point { return &Point{x, y} } +// Parses a Point from a string in the format "(X,Y)", or "POINT (X Y)". func ParsePoint(s string) (*Point, error) { var p Point - _, err := fmt.Sscanf(s, "(%f,%f)", &p.X, &p.Y) + var err error + if strings.HasPrefix(s, "POINT (") { + _, err = fmt.Sscanf(s, "POINT (%f %f)", &p.X, &p.Y) + } else { + _, err = fmt.Sscanf(s, "(%f,%f)", &p.X, &p.Y) + } return &p, err } diff --git a/sdk/go/pkg/postgresql/point_test.go b/sdk/go/pkg/db/point_test.go similarity index 77% rename from sdk/go/pkg/postgresql/point_test.go rename to sdk/go/pkg/db/point_test.go index f21faa9fa..35f339e0f 100644 --- a/sdk/go/pkg/postgresql/point_test.go +++ b/sdk/go/pkg/db/point_test.go @@ -7,17 +7,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -package postgresql_test +package db_test import ( "encoding/json" "testing" - "github.com/hypermodeinc/modus/sdk/go/pkg/postgresql" + "github.com/hypermodeinc/modus/sdk/go/pkg/db" ) func TestPointString(t *testing.T) { - point := postgresql.NewPoint(12.345678901234567, -56.7890123456789) + point := db.NewPoint(12.345678901234567, -56.7890123456789) expected := "(12.345678901234567,-56.7890123456789)" result := point.String() @@ -27,7 +27,7 @@ func TestPointString(t *testing.T) { } func TestPointMarshalJSON(t *testing.T) { - point := postgresql.NewPoint(12.345678901234567, -56.7890123456789) + point := db.NewPoint(12.345678901234567, -56.7890123456789) expected := `"(12.345678901234567,-56.7890123456789)"` result, err := json.Marshal(point) @@ -42,8 +42,8 @@ func TestPointMarshalJSON(t *testing.T) { func TestPointUnmarshalJSON(t *testing.T) { data := []byte(`"(12.345678901234567,-56.7890123456789)"`) - expected := postgresql.NewPoint(12.345678901234567, -56.7890123456789) - point := &postgresql.Point{} + expected := db.NewPoint(12.345678901234567, -56.7890123456789) + point := &db.Point{} err := json.Unmarshal(data, point) @@ -59,7 +59,7 @@ func TestPointUnmarshalJSON(t *testing.T) { func TestNewPoint(t *testing.T) { x := 12.345678901234567 y := -56.7890123456789 - point := postgresql.NewPoint(x, y) + point := db.NewPoint(x, y) if point.X != x || point.Y != y { t.Errorf("Expected X: %f, Y: %f, but got %+v", x, y, point) @@ -68,8 +68,8 @@ func TestNewPoint(t *testing.T) { func TestParsePoint(t *testing.T) { s := "(12.345678901234567,-56.7890123456789)" - expected := postgresql.NewPoint(12.345678901234567, -56.7890123456789) - point, err := postgresql.ParsePoint(s) + expected := db.NewPoint(12.345678901234567, -56.7890123456789) + point, err := db.ParsePoint(s) if err != nil { t.Errorf("Unexpected error: %v", err) @@ -82,7 +82,7 @@ func TestParsePoint(t *testing.T) { func TestParsePointError(t *testing.T) { s := "invalid-point" - _, err := postgresql.ParsePoint(s) + _, err := db.ParsePoint(s) if err == nil { t.Error("Expected an error, but received none") diff --git a/sdk/go/pkg/mysql/mysql.go b/sdk/go/pkg/mysql/mysql.go new file mode 100644 index 000000000..916e074f7 --- /dev/null +++ b/sdk/go/pkg/mysql/mysql.go @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Hypermode Inc. + * Licensed under the terms of the Apache License, Version 2.0 + * See the LICENSE file that accompanied this code for further details. + * + * SPDX-FileCopyrightText: 2024 Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package mysql + +import "github.com/hypermodeinc/modus/sdk/go/pkg/db" + +const dbType = "mysql" + +// Executes a database query that does not return rows. +func Execute(hostName, statement string, params ...any) (*db.Response, error) { + return db.Execute(hostName, dbType, statement, params...) +} + +// Executes a database query that returns rows. +// The structure of the rows is determined by the type parameter. +func Query[T any](hostName, statement string, params ...any) (*db.QueryResponse[T], error) { + return db.Query[T](hostName, dbType, statement, params...) +} + +// Executes a database query that returns a single scalar value. +// The type parameter determines the type of the scalar value. +func QueryScalar[T any](hostName, statement string, params ...any) (*db.ScalarResponse[T], error) { + return db.QueryScalar[T](hostName, dbType, statement, params...) +} + +// Represents a point in 2D space, having X and Y coordinates. +// Correctly serializes to and from a SQL point type, in (X, Y) order. +// +// Note that this struct is identical to the Location struct, but uses different field names. +type Point = db.Point + +// Represents a location on Earth, having longitude and latitude coordinates. +// Correctly serializes to and from a SQL point type, in (longitude, latitude) order. +// +// Note that this struct is identical to the Point struct, but uses different field names. +type Location = db.Location + +// Creates a new Point with the specified X and Y coordinates. +func NewPoint(x, y float64) *Point { + return db.NewPoint(x, y) +} + +// Creates a new Location with the specified longitude and latitude coordinates. +func NewLocation(longitude, latitude float64) *Location { + return db.NewLocation(longitude, latitude) +} diff --git a/sdk/go/pkg/postgresql/postgresql.go b/sdk/go/pkg/postgresql/postgresql.go index ced4a7f45..ac2d43306 100644 --- a/sdk/go/pkg/postgresql/postgresql.go +++ b/sdk/go/pkg/postgresql/postgresql.go @@ -13,14 +13,41 @@ import "github.com/hypermodeinc/modus/sdk/go/pkg/db" const dbType = "postgresql" -func Query[T any](hostName, statement string, params ...any) ([]T, uint, error) { +// Executes a database query that does not return rows. +func Execute(hostName, statement string, params ...any) (*db.Response, error) { + return db.Execute(hostName, dbType, statement, params...) +} + +// Executes a database query that returns rows. +// The structure of the rows is determined by the type parameter. +func Query[T any](hostName, statement string, params ...any) (*db.QueryResponse[T], error) { return db.Query[T](hostName, dbType, statement, params...) } -func QueryScalar[T any](hostName, statement string, params ...any) (T, uint, error) { +// Executes a database query that returns a single scalar value. +// The type parameter determines the type of the scalar value. +func QueryScalar[T any](hostName, statement string, params ...any) (*db.ScalarResponse[T], error) { return db.QueryScalar[T](hostName, dbType, statement, params...) } -func Execute(hostName, statement string, params ...any) (uint, error) { - return db.Execute(hostName, dbType, statement, params...) +// Represents a point in 2D space, having X and Y coordinates. +// Correctly serializes to and from a SQL point type, in (X, Y) order. +// +// Note that this struct is identical to the Location struct, but uses different field names. +type Point = db.Point + +// Represents a location on Earth, having longitude and latitude coordinates. +// Correctly serializes to and from a SQL point type, in (longitude, latitude) order. +// +// Note that this struct is identical to the Point struct, but uses different field names. +type Location = db.Location + +// Creates a new Point with the specified X and Y coordinates. +func NewPoint(x, y float64) *Point { + return db.NewPoint(x, y) +} + +// Creates a new Location with the specified longitude and latitude coordinates. +func NewLocation(longitude, latitude float64) *Location { + return db.NewLocation(longitude, latitude) }