diff --git a/.aspire/settings.json b/.aspire/settings.json new file mode 100644 index 0000000000..61dd69a516 --- /dev/null +++ b/.aspire/settings.json @@ -0,0 +1,3 @@ +{ + "appHostPath": "../src/Aspire.AppHost/Aspire.AppHost.csproj" +} \ No newline at end of file diff --git a/src/.editorconfig b/src/.editorconfig index 5639a063ae..6367bdcb3c 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -75,9 +75,9 @@ dotnet_style_allow_statement_immediately_after_block_experimental = false:error #### C# Coding Conventions #### # var preferences -csharp_style_var_elsewhere = false:error -csharp_style_var_for_built_in_types = false:error -csharp_style_var_when_type_is_apparent = false:error +# csharp_style_var_elsewhere = false:error +# csharp_style_var_for_built_in_types = false:error +# csharp_style_var_when_type_is_apparent = false:error # Modifier preferences csharp_prefer_static_local_function = true:suggestion diff --git a/src/Aspire.AppHost/AppHost.cs b/src/Aspire.AppHost/AppHost.cs new file mode 100644 index 0000000000..5477c5c81b --- /dev/null +++ b/src/Aspire.AppHost/AppHost.cs @@ -0,0 +1,95 @@ +using Microsoft.Extensions.DependencyInjection; + +var builder = DistributedApplication.CreateBuilder(args); + +var aspireDB = Environment.GetEnvironmentVariable("ASPIRE_DATABASE_TYPE"); + +var databaseConnectionString = Environment.GetEnvironmentVariable("ASPIRE_DATABASE_CONNECTION_STRING") ?? ""; + +switch (aspireDB) +{ + case "mssql": + var sqlScript = File.ReadAllText("./init-scripts/sql/create-database.sql"); + + IResourceBuilder? sqlDbContainer = null; + + if (string.IsNullOrEmpty(databaseConnectionString)) + { + Console.WriteLine("No connection string provided, starting a local SQL Server container."); + + sqlDbContainer = builder.AddSqlServer("sqlserver") + .WithDataVolume() + .WithLifetime(ContainerLifetime.Persistent) + .AddDatabase("msSqlDb", "Trek") + .WithCreationScript(sqlScript); + } + + var mssqlService = builder.AddProject("mssql-service", "Development") + .WithArgs("-f", "net8.0") + .WithEndpoint(endpointName: "https", (e) => e.Port = 1234) + .WithEndpoint(endpointName: "http", (e) => e.Port = 2345) + .WithEnvironment("db-type", "mssql") + .WithUrls((e) => + { + e.Urls.Clear(); + e.Urls.Add(new() { Url = "/swagger", DisplayText = "🔒Swagger", Endpoint = e.GetEndpoint("https") }); + e.Urls.Add(new() { Url = "/graphql", DisplayText = "🔒GraphQL", Endpoint = e.GetEndpoint("https") }); + }) + .WithHttpHealthCheck("/health"); + + if (sqlDbContainer is null) + { + mssqlService.WithEnvironment("ConnectionStrings__Database", databaseConnectionString); + } + else + { + mssqlService.WithEnvironment("ConnectionStrings__Database", sqlDbContainer) + .WaitFor(sqlDbContainer); + } + + break; + case "postgresql": + var pgScript = File.ReadAllText("./init-scripts/pg/create-database-pg.sql"); + + IResourceBuilder? postgresDB = null; + + if (!string.IsNullOrEmpty(databaseConnectionString)) + { + Console.WriteLine("No connection string provided, starting a local PostgreSQL container."); + + postgresDB = builder.AddPostgres("postgres") + .WithPgAdmin() + .WithLifetime(ContainerLifetime.Persistent) + .AddDatabase("pgDb", "postgres") + .WithCreationScript(pgScript); + } + + var pgService = builder.AddProject("pg-service", "Development") + .WithArgs("-f", "net8.0") + .WithEndpoint(endpointName: "https", (e) => e.Port = 1234) + .WithEndpoint(endpointName: "http", (e) => e.Port = 2345) + .WithEnvironment("db-type", "postgresql") + .WithUrls((e) => + { + e.Urls.Clear(); + e.Urls.Add(new() { Url = "/swagger", DisplayText = "🔒Swagger", Endpoint = e.GetEndpoint("https") }); + e.Urls.Add(new() { Url = "/graphql", DisplayText = "🔒GraphQL", Endpoint = e.GetEndpoint("https") }); + }) + .WithHttpHealthCheck("/health"); + + if (postgresDB is null) + { + pgService.WithEnvironment("ConnectionStrings__Database", databaseConnectionString); + } + else + { + pgService.WithEnvironment("ConnectionStrings__Database", postgresDB) + .WaitFor(postgresDB); + } + + break; + default: + throw new Exception("Please set the ASPIRE_DATABASE environment variable to either 'mssql' or 'postgresql'."); +} + +builder.Build().Run(); diff --git a/src/Aspire.AppHost/Aspire.AppHost.csproj b/src/Aspire.AppHost/Aspire.AppHost.csproj new file mode 100644 index 0000000000..79d44e51a1 --- /dev/null +++ b/src/Aspire.AppHost/Aspire.AppHost.csproj @@ -0,0 +1,24 @@ + + + + + + Exe + net8.0 + enable + NU1603;NU1605 + enable + f08719fd-267f-459e-9980-77b1c52c8755 + + + + + + + + + + + + + diff --git a/src/Aspire.AppHost/DockerStatus.cs b/src/Aspire.AppHost/DockerStatus.cs new file mode 100644 index 0000000000..ad4237357e --- /dev/null +++ b/src/Aspire.AppHost/DockerStatus.cs @@ -0,0 +1,26 @@ +using System.Diagnostics; + +public static class DockerStatus +{ + public static async Task IsDockerRunningAsync() + { + var psi = new ProcessStartInfo + { + FileName = "docker", + Arguments = "info", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }; + try + { + using var process = Process.Start(psi)!; + await process.WaitForExitAsync(); + return process.ExitCode == 0; + } + catch + { + return false; + } + } +} diff --git a/src/Aspire.AppHost/Properties/launchSettings.json b/src/Aspire.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000000..43a2673a34 --- /dev/null +++ b/src/Aspire.AppHost/Properties/launchSettings.json @@ -0,0 +1,57 @@ +{ + "profiles": { + "https": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21213", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22145" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:17047;http://localhost:15161" + }, + "http": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19015", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20166" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:15161" + }, + "aspire-mssql": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21213", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22145", + "ASPIRE_DATABASE_TYPE": "mssql", + "ASPIRE_DATABASE_CONNECTION_STRING": "" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:17047;http://localhost:15161" + }, + "aspire-postgresql": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21213", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22145", + "ASPIRE_DATABASE_TYPE": "postgresql", + "ASPIRE_DATABASE_CONNECTION_STRING": "" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:17047;http://localhost:15161" + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json" +} diff --git a/src/Aspire.AppHost/README.md b/src/Aspire.AppHost/README.md new file mode 100644 index 0000000000..3437c367bb --- /dev/null +++ b/src/Aspire.AppHost/README.md @@ -0,0 +1,17 @@ +# Aspire Instructions + +This project allows you to run DAB in debug mode using [Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview). + +## Prerequisites +- [.NET SDK](https://dotnet.microsoft.com/download) (8.0 or later) +- [Docker](https://www.docker.com/products/docker-desktop) (optional, for containerized development) + +## Database Configuration + +In the `launchProfile.json` file, you can configure the database connection string. If you don't, Aspire will start for you a local instance in a Docker container. + +Simply provide a value for the `ASPIRE_DATABASE_CONNECTION_STRING` environment variable. + +You can select to run Aspire with different databases selecting the appropriate launch profile: +- `aspire-sql` +- `aspire-postgres` diff --git a/src/Aspire.AppHost/appsettings.json b/src/Aspire.AppHost/appsettings.json new file mode 100644 index 0000000000..31c092aa45 --- /dev/null +++ b/src/Aspire.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/src/Aspire.AppHost/init-scripts/pg/create-database-pg.sql b/src/Aspire.AppHost/init-scripts/pg/create-database-pg.sql new file mode 100644 index 0000000000..73adccc008 --- /dev/null +++ b/src/Aspire.AppHost/init-scripts/pg/create-database-pg.sql @@ -0,0 +1,238 @@ +-- create-database-pg.sql +-- PostgreSQL version of create-database.sql + +-- Create database (run this as a superuser, outside the target database) +-- Uncomment and edit the database name as needed +--CREATE DATABASE "Trek"; + +-- Connect to the target database before running the rest of the script +--\connect Trek; + +-- Drop tables in reverse order of creation due to foreign key dependencies +DROP TABLE IF EXISTS "Character_Species"; +DROP TABLE IF EXISTS "Series_Character"; +DROP TABLE IF EXISTS "Character"; +DROP TABLE IF EXISTS "Species"; +DROP TABLE IF EXISTS "Actor"; +DROP TABLE IF EXISTS "Series"; + +-- create tables +CREATE TABLE "Series" ( + "Id" INTEGER PRIMARY KEY, + "Name" VARCHAR(255) NOT NULL +); + +CREATE TABLE "Actor" ( + "Id" INTEGER PRIMARY KEY, + "Name" VARCHAR(255) NOT NULL, + "BirthYear" INTEGER NOT NULL +); + +CREATE TABLE "Species" ( + "Id" INTEGER PRIMARY KEY, + "Name" VARCHAR(255) NOT NULL +); + +CREATE TABLE "Character" ( + "Id" INTEGER PRIMARY KEY, + "Name" VARCHAR(255) NOT NULL, + "ActorId" INTEGER NOT NULL, + "Stardate" DECIMAL(10, 2), + FOREIGN KEY ("ActorId") REFERENCES "Actor"("Id") +); + +CREATE TABLE "Series_Character" ( + "SeriesId" INTEGER, + "CharacterId" INTEGER, + "Role" VARCHAR(500), + FOREIGN KEY ("SeriesId") REFERENCES "Series"("Id"), + FOREIGN KEY ("CharacterId") REFERENCES "Character"("Id"), + PRIMARY KEY ("SeriesId", "CharacterId") +); + +CREATE TABLE "Character_Species" ( + "CharacterId" INTEGER, + "SpeciesId" INTEGER, + FOREIGN KEY ("CharacterId") REFERENCES "Character"("Id"), + FOREIGN KEY ("SpeciesId") REFERENCES "Species"("Id"), + PRIMARY KEY ("CharacterId", "SpeciesId") +); + +-- create data +INSERT INTO "Series" ("Id", "Name") VALUES + (1, 'Star Trek'), + (2, 'Star Trek: The Next Generation'), + (3, 'Star Trek: Voyager'), + (4, 'Star Trek: Deep Space Nine'), + (5, 'Star Trek: Enterprise'); + +INSERT INTO "Species" ("Id", "Name") VALUES + (1, 'Human'), + (2, 'Vulcan'), + (3, 'Android'), + (4, 'Klingon'), + (5, 'Betazoid'), + (6, 'Hologram'), + (7, 'Bajoran'), + (8, 'Changeling'), + (9, 'Trill'), + (10, 'Ferengi'), + (11, 'Denobulan'), + (12, 'Borg'); + +INSERT INTO "Actor" ("Id", "Name", "BirthYear") VALUES + (1, 'William Shatner', 1931), + (2, 'Leonard Nimoy', 1931), + (3, 'DeForest Kelley', 1920), + (4, 'James Doohan', 1920), + (5, 'Nichelle Nichols', 1932), + (6, 'George Takei', 1937), + (7, 'Walter Koenig', 1936), + (8, 'Patrick Stewart', 1940), + (9, 'Jonathan Frakes', 1952), + (10, 'Brent Spiner', 1949), + (11, 'Michael Dorn', 1952), + (12, 'Gates McFadden', 1949), + (13, 'Marina Sirtis', 1955), + (14, 'LeVar Burton', 1957), + (15, 'Kate Mulgrew', 1955), + (16, 'Robert Beltran', 1953), + (17, 'Tim Russ', 1956), + (18, 'Roxann Dawson', 1958), + (19, 'Robert Duncan McNeill', 1964), + (20, 'Garrett Wang', 1968), + (21, 'Robert Picardo', 1953), + (22, 'Jeri Ryan', 1968), + (23, 'Avery Brooks', 1948), + (24, 'Nana Visitor', 1957), + (25, 'Rene Auberjonois', 1940), + (26, 'Terry Farrell', 1963), + (27, 'Alexander Siddig', 1965), + (28, 'Armin Shimerman', 1949), + (29, 'Cirroc Lofton', 1978), + (30, 'Scott Bakula', 1954), + (31, 'Jolene Blalock', 1975), + (32, 'John Billingsley', 1960), + (33, 'Connor Trinneer', 1969), + (34, 'Dominic Keating', 1962), + (35, 'Linda Park', 1978), + (36, 'Anthony Montgomery', 1971); + +INSERT INTO "Character" ("Id", "Name", "ActorId", "Stardate") VALUES + (1, 'James T. Kirk', 1, 2233.04), + (2, 'Spock', 2, 2230.06), + (3, 'Leonard McCoy', 3, 2227.00), + (4, 'Montgomery Scott', 4, 2222.00), + (5, 'Uhura', 5, 2233.00), + (6, 'Hikaru Sulu', 6, 2237.00), + (7, 'Pavel Chekov', 7, 2245.00), + (8, 'Jean-Luc Picard', 8, 2305.07), + (9, 'William Riker', 9, 2335.08), + (10, 'Data', 10, 2336.00), + (11, 'Worf', 11, 2340.00), + (12, 'Beverly Crusher', 12, 2324.00), + (13, 'Deanna Troi', 13, 2336.00), + (14, 'Geordi La Forge', 14, 2335.02), + (15, 'Kathryn Janeway', 15, 2336.05), + (16, 'Chakotay', 16, 2329.00), + (17, 'Tuvok', 17, 2264.00), + (18, 'B''Elanna Torres', 18, 2349.00), + (19, 'Tom Paris', 19, 2346.00), + (20, 'Harry Kim', 20, 2349.00), + (21, 'The Doctor', 21, 2371.00), -- Stardate of activation + (22, 'Seven of Nine', 22, 2348.00), + (23, 'Benjamin Sisko', 23, 2332.00), + (24, 'Kira Nerys', 24, 2343.00), + (25, 'Odo', 25, 2337.00), -- Approximate stardate of discovery + (27, 'Jadzia Dax', 26, 2341.00), + (28, 'Julian Bashir', 27, 2341.00), + (29, 'Quark', 28, 2333.00), + (30, 'Jake Sisko', 29, 2355.00), + (31, 'Jonathan Archer', 30, 2112.00), + (32, 'T''Pol', 31, 2088.00), + (33, 'Phlox', 32, 2102.00), + (34, 'Charles "Trip" Tucker III', 33, 2121.00), + (35, 'Malcolm Reed', 34, 2117.00), + (36, 'Hoshi Sato', 35, 2129.00), + (37, 'Travis Mayweather', 36, 2126.00); + +INSERT INTO "Series_Character" ("SeriesId", "CharacterId", "Role") VALUES + (1, 1, 'Captain'), -- James T. Kirk in Star Trek + (1, 2, 'Science Officer'), -- Spock in Star Trek + (1, 3, 'Doctor'), -- Leonard McCoy in Star Trek + (1, 4, 'Engineer'), -- Montgomery Scott in Star Trek + (1, 5, 'Communications Officer'), -- Uhura in Star Trek + (1, 6, 'Helmsman'), -- Hikaru Sulu in Star Trek + (1, 7, 'Navigator'), -- Pavel Chekov in Star Trek + (2, 8, 'Captain'), -- Jean-Luc Picard in Star Trek: The Next Generation + (2, 9, 'First Officer'), -- William Riker in Star Trek: The Next Generation + (2, 10, 'Operations Officer'),-- Data in Star Trek: The Next Generation + (2, 11, 'Security Officer'),-- Worf in Star Trek: The Next Generation + (2, 12, 'Doctor'),-- Beverly Crusher in Star Trek: The Next Generation + (2, 13, 'Counselor'),-- Deanna Troi in Star Trek: The Next Generation + (2, 14, 'Engineer'),-- Geordi La Forge in Star Trek: The Next Generation + (3, 15, 'Captain'),-- Kathryn Janeway in Star Trek: Voyager + (3, 16, 'First Officer'),-- Chakotay in Star Trek: Voyager + (3, 17, 'Tactical Officer'),-- Tuvok in Star Trek: Voyager + (3, 18, 'Engineer'),-- B'Elanna Torres in Star Trek: Voyager + (3, 19, 'Helmsman'),-- Tom Paris in Star Trek: Voyager + (3, 20, 'Operations Officer'),-- Harry Kim in Star Trek: Voyager + (3, 21, 'Doctor'),-- The Doctor in Star Trek: Voyager + (3, 22, 'Astrometrics Officer'),-- Seven of Nine in Star Trek: Voyager + (4, 23, 'Commanding Officer'),-- Benjamin Sisko in Star Trek: Deep Space Nine + (4, 24, 'First Officer'),-- Kira Nerys in Star Trek: Deep Space Nine + (4, 25, 'Security Officer'),-- Odo in Star Trek: Deep Space Nine + (4, 11, 'Strategic Operations Officer'),-- Worf in Star Trek: Deep Space Nine + (4, 27, 'Science Officer'),-- Jadzia Dax in Star Trek: Deep Space Nine + (4, 28, 'Doctor'),-- Julian Bashir in Star Trek: Deep Space Nine + (4, 29, 'Bar Owner'),-- Quark in Star Trek: Deep Space Nine + (4, 30, 'Civilian'),-- Jake Sisko in Star Trek: Deep Space Nine + (5, 31, 'Captain'),-- Jonathan Archer in Star Trek: Enterprise + (5, 32, 'Science Officer'),-- T'Pol in Star Trek: Enterprise + (5, 33, 'Doctor'),-- Phlox in Star Trek: Enterprise + (5, 34, 'Chief Engineer'),-- Charles "Trip" Tucker III in Star Trek: Enterprise + (5, 35, 'Armory Officer'),-- Malcolm Reed in Star Trek: Enterprise + (5, 36, 'Communications Officer'),-- Hoshi Sato in Star Trek: Enterprise + (5, 37, 'Helmsman');-- Travis Mayweather in Star Trek: Enterprise + +INSERT INTO "Character_Species" ("CharacterId", "SpeciesId") VALUES + (1, 1), -- James T. Kirk is Human + (2, 2), -- Spock is Vulcan + (2, 1), -- Spock is also Human + (3, 1), -- Leonard McCoy is Human + (4, 1), -- Montgomery Scott is Human + (5, 1), -- Uhura is Human + (6, 1), -- Hikaru Sulu is Human + (7, 1), -- Pavel Chekov is Human + (8, 1), -- Jean-Luc Picard is Human + (9, 1), -- William Riker is Human + (10, 3), -- Data is Android + (11, 4), -- Worf is Klingon + (12, 1), -- Beverly Crusher is Human + (13, 1), -- Deanna Troi is Human + (13, 5), -- Deanna Troi is also Betazoid + (14, 1), -- Geordi La Forge is Human + (15, 1), -- Kathryn Janeway is Human + (16, 1), -- Chakotay is Human + (17, 2), -- Tuvok is Vulcan + (18, 1), -- B'Elanna Torres is Human + (18, 4), -- B'Elanna Torres is also Klingon + (19, 1), -- Tom Paris is Human + (20, 1), -- Harry Kim is Human + (21, 6), -- The Doctor is a Hologram + (22, 1), -- Seven of Nine is Human + (22, 12),-- Seven of Nine is also Borg + (23, 1), -- Benjamin Sisko is Human + (24, 7), -- Kira Nerys is Bajoran + (25, 8), -- Odo is Changeling + (27, 9), -- Jadzia Dax is Trill + (28, 1), -- Julian Bashir is Human + (29, 10),-- Quark is Ferengi + (30, 1), -- Jake Sisko is Human + (31, 1), -- Jonathan Archer is Human + (32, 2), -- T'Pol is Vulcan + (33, 11),-- Phlox is Denobulan + (34, 1), -- Charles "Trip" Tucker III is Human + (35, 1), -- Malcolm Reed is Human + (36, 1), -- Hoshi Sato is Human + (37, 1); -- Travis Mayweather is Human diff --git a/src/Aspire.AppHost/init-scripts/sql/create-database.sql b/src/Aspire.AppHost/init-scripts/sql/create-database.sql new file mode 100644 index 0000000000..a12c71138e --- /dev/null +++ b/src/Aspire.AppHost/init-scripts/sql/create-database.sql @@ -0,0 +1,239 @@ + +USE [master] +GO + +CREATE DATABASE [Trek] +GO + +USE [Trek] +GO + +-- Drop tables in reverse order of creation due to foreign key dependencies +DROP TABLE IF EXISTS Character_Species; +DROP TABLE IF EXISTS Series_Character; +DROP TABLE IF EXISTS Character; +DROP TABLE IF EXISTS Species; +DROP TABLE IF EXISTS Actor; +DROP TABLE IF EXISTS Series; + +-- create tables +CREATE TABLE Series ( + Id INT PRIMARY KEY, + Name NVARCHAR(255) NOT NULL +); + +CREATE TABLE Actor ( + Id INT PRIMARY KEY, + Name NVARCHAR(255) NOT NULL, + [BirthYear] INT NOT NULL +); + +CREATE TABLE Species ( + Id INT PRIMARY KEY, + Name NVARCHAR(255) NOT NULL +); + +CREATE TABLE Character ( + Id INT PRIMARY KEY, + Name NVARCHAR(255) NOT NULL, + ActorId INT NOT NULL, + Stardate DECIMAL(10, 2), + FOREIGN KEY (ActorId) REFERENCES Actor(Id) +); + +CREATE TABLE Series_Character ( + SeriesId INT, + CharacterId INT, + Role VARCHAR(500), + FOREIGN KEY (SeriesId) REFERENCES Series(Id), + FOREIGN KEY (CharacterId) REFERENCES Character(Id), + PRIMARY KEY (SeriesId, CharacterId) +); + +CREATE TABLE Character_Species ( + CharacterId INT, + SpeciesId INT, + FOREIGN KEY (CharacterId) REFERENCES Character(Id), + FOREIGN KEY (SpeciesId) REFERENCES Species(Id), + PRIMARY KEY (CharacterId, SpeciesId) +); + +-- create data +INSERT INTO Series (Id, Name) VALUES + (1, 'Star Trek'), + (2, 'Star Trek: The Next Generation'), + (3, 'Star Trek: Voyager'), + (4, 'Star Trek: Deep Space Nine'), + (5, 'Star Trek: Enterprise'); + +INSERT INTO Species (Id, Name) VALUES + (1, 'Human'), + (2, 'Vulcan'), + (3, 'Android'), + (4, 'Klingon'), + (5, 'Betazoid'), + (6, 'Hologram'), + (7, 'Bajoran'), + (8, 'Changeling'), + (9, 'Trill'), + (10, 'Ferengi'), + (11, 'Denobulan'), + (12, 'Borg'); + +INSERT INTO Actor (Id, Name, [BirthYear]) VALUES + (1, 'William Shatner', 1931), + (2, 'Leonard Nimoy', 1931), + (3, 'DeForest Kelley', 1920), + (4, 'James Doohan', 1920), + (5, 'Nichelle Nichols', 1932), + (6, 'George Takei', 1937), + (7, 'Walter Koenig', 1936), + (8, 'Patrick Stewart', 1940), + (9, 'Jonathan Frakes', 1952), + (10, 'Brent Spiner', 1949), + (11, 'Michael Dorn', 1952), + (12, 'Gates McFadden', 1949), + (13, 'Marina Sirtis', 1955), + (14, 'LeVar Burton', 1957), + (15, 'Kate Mulgrew', 1955), + (16, 'Robert Beltran', 1953), + (17, 'Tim Russ', 1956), + (18, 'Roxann Dawson', 1958), + (19, 'Robert Duncan McNeill', 1964), + (20, 'Garrett Wang', 1968), + (21, 'Robert Picardo', 1953), + (22, 'Jeri Ryan', 1968), + (23, 'Avery Brooks', 1948), + (24, 'Nana Visitor', 1957), + (25, 'Rene Auberjonois', 1940), + (26, 'Terry Farrell', 1963), + (27, 'Alexander Siddig', 1965), + (28, 'Armin Shimerman', 1949), + (29, 'Cirroc Lofton', 1978), + (30, 'Scott Bakula', 1954), + (31, 'Jolene Blalock', 1975), + (32, 'John Billingsley', 1960), + (33, 'Connor Trinneer', 1969), + (34, 'Dominic Keating', 1962), + (35, 'Linda Park', 1978), + (36, 'Anthony Montgomery', 1971); + +INSERT INTO Character (Id, Name, ActorId, Stardate) VALUES + (1, 'James T. Kirk', 1, 2233.04), + (2, 'Spock', 2, 2230.06), + (3, 'Leonard McCoy', 3, 2227.00), + (4, 'Montgomery Scott', 4, 2222.00), + (5, 'Uhura', 5, 2233.00), + (6, 'Hikaru Sulu', 6, 2237.00), + (7, 'Pavel Chekov', 7, 2245.00), + (8, 'Jean-Luc Picard', 8, 2305.07), + (9, 'William Riker', 9, 2335.08), + (10, 'Data', 10, 2336.00), + (11, 'Worf', 11, 2340.00), + (12, 'Beverly Crusher', 12, 2324.00), + (13, 'Deanna Troi', 13, 2336.00), + (14, 'Geordi La Forge', 14, 2335.02), + (15, 'Kathryn Janeway', 15, 2336.05), + (16, 'Chakotay', 16, 2329.00), + (17, 'Tuvok', 17, 2264.00), + (18, 'B''Elanna Torres', 18, 2349.00), + (19, 'Tom Paris', 19, 2346.00), + (20, 'Harry Kim', 20, 2349.00), + (21, 'The Doctor', 21, 2371.00), -- Stardate of activation + (22, 'Seven of Nine', 22, 2348.00), + (23, 'Benjamin Sisko', 23, 2332.00), + (24, 'Kira Nerys', 24, 2343.00), + (25, 'Odo', 25, 2337.00), -- Approximate stardate of discovery + (27, 'Jadzia Dax', 26, 2341.00), + (28, 'Julian Bashir', 27, 2341.00), + (29, 'Quark', 28, 2333.00), + (30, 'Jake Sisko', 29, 2355.00), + (31, 'Jonathan Archer', 30, 2112.00), + (32, 'T''Pol', 31, 2088.00), + (33, 'Phlox', 32, 2102.00), + (34, 'Charles "Trip" Tucker III', 33, 2121.00), + (35, 'Malcolm Reed', 34, 2117.00), + (36, 'Hoshi Sato', 35, 2129.00), + (37, 'Travis Mayweather', 36, 2126.00); + +INSERT INTO Series_Character (SeriesId, CharacterId, Role) VALUES + (1, 1, 'Captain'), -- James T. Kirk in Star Trek + (1, 2, 'Science Officer'), -- Spock in Star Trek + (1, 3, 'Doctor'), -- Leonard McCoy in Star Trek + (1, 4, 'Engineer'), -- Montgomery Scott in Star Trek + (1, 5, 'Communications Officer'), -- Uhura in Star Trek + (1, 6, 'Helmsman'), -- Hikaru Sulu in Star Trek + (1, 7, 'Navigator'), -- Pavel Chekov in Star Trek + (2, 8, 'Captain'), -- Jean-Luc Picard in Star Trek: The Next Generation + (2, 9, 'First Officer'), -- William Riker in Star Trek: The Next Generation + (2, 10, 'Operations Officer'),-- Data in Star Trek: The Next Generation + (2, 11, 'Security Officer'),-- Worf in Star Trek: The Next Generation + (2, 12, 'Doctor'),-- Beverly Crusher in Star Trek: The Next Generation + (2, 13, 'Counselor'),-- Deanna Troi in Star Trek: The Next Generation + (2, 14, 'Engineer'),-- Geordi La Forge in Star Trek: The Next Generation + (3, 15, 'Captain'),-- Kathryn Janeway in Star Trek: Voyager + (3, 16, 'First Officer'),-- Chakotay in Star Trek: Voyager + (3, 17, 'Tactical Officer'),-- Tuvok in Star Trek: Voyager + (3, 18, 'Engineer'),-- B'Elanna Torres in Star Trek: Voyager + (3, 19, 'Helmsman'),-- Tom Paris in Star Trek: Voyager + (3, 20, 'Operations Officer'),-- Harry Kim in Star Trek: Voyager + (3, 21, 'Doctor'),-- The Doctor in Star Trek: Voyager + (3, 22, 'Astrometrics Officer'),-- Seven of Nine in Star Trek: Voyager + (4, 23, 'Commanding Officer'),-- Benjamin Sisko in Star Trek: Deep Space Nine + (4, 24, 'First Officer'),-- Kira Nerys in Star Trek: Deep Space Nine + (4, 25, 'Security Officer'),-- Odo in Star Trek: Deep Space Nine + (4, 11, 'Strategic Operations Officer'),-- Worf in Star Trek: Deep Space Nine + (4, 27, 'Science Officer'),-- Jadzia Dax in Star Trek: Deep Space Nine + (4, 28, 'Doctor'),-- Julian Bashir in Star Trek: Deep Space Nine + (4, 29, 'Bar Owner'),-- Quark in Star Trek: Deep Space Nine + (4, 30, 'Civilian'),-- Jake Sisko in Star Trek: Deep Space Nine + (5, 31, 'Captain'),-- Jonathan Archer in Star Trek: Enterprise + (5, 32, 'Science Officer'),-- T'Pol in Star Trek: Enterprise + (5, 33, 'Doctor'),-- Phlox in Star Trek: Enterprise + (5, 34, 'Chief Engineer'),-- Charles "Trip" Tucker III in Star Trek: Enterprise + (5, 35, 'Armory Officer'),-- Malcolm Reed in Star Trek: Enterprise + (5, 36, 'Communications Officer'),-- Hoshi Sato in Star Trek: Enterprise + (5, 37, 'Helmsman');-- Travis Mayweather in Star Trek: Enterprise + +INSERT INTO Character_Species (CharacterId, SpeciesId) VALUES + (1, 1), -- James T. Kirk is Human + (2, 2), -- Spock is Vulcan + (2, 1), -- Spock is also Human + (3, 1), -- Leonard McCoy is Human + (4, 1), -- Montgomery Scott is Human + (5, 1), -- Uhura is Human + (6, 1), -- Hikaru Sulu is Human + (7, 1), -- Pavel Chekov is Human + (8, 1), -- Jean-Luc Picard is Human + (9, 1), -- William Riker is Human + (10, 3), -- Data is Android + (11, 4), -- Worf is Klingon + (12, 1), -- Beverly Crusher is Human + (13, 1), -- Deanna Troi is Human + (13, 5), -- Deanna Troi is also Betazoid + (14, 1), -- Geordi La Forge is Human + (15, 1), -- Kathryn Janeway is Human + (16, 1), -- Chakotay is Human + (17, 2), -- Tuvok is Vulcan + (18, 1), -- B'Elanna Torres is Human + (18, 4), -- B'Elanna Torres is also Klingon + (19, 1), -- Tom Paris is Human + (20, 1), -- Harry Kim is Human + (21, 6), -- The Doctor is a Hologram + (22, 1), -- Seven of Nine is Human + (22, 12),-- Seven of Nine is also Borg + (23, 1), -- Benjamin Sisko is Human + (24, 7), -- Kira Nerys is Bajoran + (25, 8), -- Odo is Changeling + (27, 9), -- Jadzia Dax is Trill + (28, 1), -- Julian Bashir is Human + (29, 10),-- Quark is Ferengi + (30, 1), -- Jake Sisko is Human + (31, 1), -- Jonathan Archer is Human + (32, 2), -- T'Pol is Vulcan + (33, 11),-- Phlox is Denobulan + (34, 1), -- Charles "Trip" Tucker III is Human + (35, 1), -- Malcolm Reed is Human + (36, 1), -- Hoshi Sato is Human + (37, 1); -- Travis Mayweather is Human + \ No newline at end of file diff --git a/src/Azure.DataApiBuilder.sln b/src/Azure.DataApiBuilder.sln index aa3c8e2bad..58fe70b07c 100644 --- a/src/Azure.DataApiBuilder.sln +++ b/src/Azure.DataApiBuilder.sln @@ -1,3 +1,4 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.3.32405.409 @@ -31,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.DataApiBuilder.Core", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.DataApiBuilder.Product", "Product\Azure.DataApiBuilder.Product.csproj", "{E3D2076C-EE49-43A0-8F92-5FC41EC99DA7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.AppHost", "Aspire.AppHost\Aspire.AppHost.csproj", "{87B53030-EB52-4EB1-870D-72540DA4724E}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.DataApiBuilder.Mcp", "Azure.DataApiBuilder.Mcp\Azure.DataApiBuilder.Mcp.csproj", "{A287E849-A043-4F37-BC40-A87C4705F583}" EndProject Global @@ -75,6 +78,10 @@ Global {E3D2076C-EE49-43A0-8F92-5FC41EC99DA7}.Debug|Any CPU.Build.0 = Debug|Any CPU {E3D2076C-EE49-43A0-8F92-5FC41EC99DA7}.Release|Any CPU.ActiveCfg = Release|Any CPU {E3D2076C-EE49-43A0-8F92-5FC41EC99DA7}.Release|Any CPU.Build.0 = Release|Any CPU + {87B53030-EB52-4EB1-870D-72540DA4724E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87B53030-EB52-4EB1-870D-72540DA4724E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87B53030-EB52-4EB1-870D-72540DA4724E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87B53030-EB52-4EB1-870D-72540DA4724E}.Release|Any CPU.Build.0 = Release|Any CPU {A287E849-A043-4F37-BC40-A87C4705F583}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A287E849-A043-4F37-BC40-A87C4705F583}.Debug|Any CPU.Build.0 = Debug|Any CPU {A287E849-A043-4F37-BC40-A87C4705F583}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 14f097915c..658e489bac 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -3,7 +3,11 @@ true - + + + + + @@ -29,6 +33,9 @@ + + + diff --git a/src/Service/Azure.DataApiBuilder.Service.csproj b/src/Service/Azure.DataApiBuilder.Service.csproj index 6ea9c8dad2..5cf762ca57 100644 --- a/src/Service/Azure.DataApiBuilder.Service.csproj +++ b/src/Service/Azure.DataApiBuilder.Service.csproj @@ -1,7 +1,7 @@  - net8.0 + net8.0 Debug;Release;Docker $(BaseOutputPath)\engine win-x64;linux-x64;osx-x64 diff --git a/src/Service/HealthCheck/HealthCheckHelper.cs b/src/Service/HealthCheck/HealthCheckHelper.cs index 452cb803a9..9b7bbd272c 100644 --- a/src/Service/HealthCheck/HealthCheckHelper.cs +++ b/src/Service/HealthCheck/HealthCheckHelper.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Data.Common; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; @@ -161,7 +162,7 @@ private async Task UpdateDataSourceHealthCheckResultsAsync(ComprehensiveHealthCh if (comprehensiveHealthCheckReport.Checks != null && runtimeConfig.DataSource.IsDatasourceHealthEnabled) { string query = Utilities.GetDatSourceQuery(runtimeConfig.DataSource.DatabaseType); - (int, string?) response = await ExecuteDatasourceQueryCheckAsync(query, runtimeConfig.DataSource.ConnectionString); + (int, string?) response = await ExecuteDatasourceQueryCheckAsync(query, runtimeConfig.DataSource.ConnectionString, Utilities.GetDbProviderFactory(runtimeConfig.DataSource.DatabaseType)); bool isResponseTimeWithinThreshold = response.Item1 >= 0 && response.Item1 < runtimeConfig.DataSource.DatasourceThresholdMs; // Add DataSource Health Check Results @@ -181,14 +182,14 @@ private async Task UpdateDataSourceHealthCheckResultsAsync(ComprehensiveHealthCh } // Executes the DB Query and keeps track of the response time and error message. - private async Task<(int, string?)> ExecuteDatasourceQueryCheckAsync(string query, string connectionString) + private async Task<(int, string?)> ExecuteDatasourceQueryCheckAsync(string query, string connectionString, DbProviderFactory dbProviderFactory) { string? errorMessage = null; if (!string.IsNullOrEmpty(query) && !string.IsNullOrEmpty(connectionString)) { Stopwatch stopwatch = new(); stopwatch.Start(); - errorMessage = await _httpUtility.ExecuteDbQueryAsync(query, connectionString); + errorMessage = await _httpUtility.ExecuteDbQueryAsync(query, connectionString, dbProviderFactory); stopwatch.Stop(); return string.IsNullOrEmpty(errorMessage) ? ((int)stopwatch.ElapsedMilliseconds, errorMessage) : (HealthCheckConstants.ERROR_RESPONSE_TIME_MS, errorMessage); } diff --git a/src/Service/HealthCheck/HttpUtilities.cs b/src/Service/HealthCheck/HttpUtilities.cs index a3195d0d8a..9da596ae30 100644 --- a/src/Service/HealthCheck/HttpUtilities.cs +++ b/src/Service/HealthCheck/HttpUtilities.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Data.Common; using System.Linq; using System.Net.Http; using System.Text; @@ -14,7 +15,6 @@ using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Core.Services.MetadataProviders; using Azure.DataApiBuilder.Service.GraphQLBuilder; -using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; namespace Azure.DataApiBuilder.Service.HealthCheck @@ -49,19 +49,32 @@ public HttpUtilities( } // Executes the DB query by establishing a connection to the DB. - public async Task ExecuteDbQueryAsync(string query, string connectionString) + public async Task ExecuteDbQueryAsync(string query, string connectionString, DbProviderFactory providerFactory) { string? errorMessage = null; // Execute the query on DB and return the response time. - using (SqlConnection connection = new(connectionString)) + DbConnection? connection = providerFactory.CreateConnection(); + if (connection == null) + { + errorMessage = "Failed to create database connection."; + _logger.LogError(errorMessage); + return errorMessage; + } + + using (connection) { try { - SqlCommand command = new(query, connection); - connection.Open(); - SqlDataReader reader = await command.ExecuteReaderAsync(); - _logger.LogTrace("The health check query for datasource executed successfully."); - reader.Close(); + connection.ConnectionString = connectionString; + using (DbCommand command = connection.CreateCommand()) + { + command.CommandText = query; + await connection.OpenAsync(); + using (DbDataReader reader = await command.ExecuteReaderAsync()) + { + _logger.LogTrace("The health check query for datasource executed successfully."); + } + } } catch (Exception ex) { @@ -145,7 +158,7 @@ public HttpUtilities( List columnNames = dbObject.SourceDefinition.Columns.Keys.ToList(); // In case of GraphQL API, use the plural value specified in [entity.graphql.type.plural]. - // Further, we need to camel case this plural value to match the GraphQL object name. + // Further, we need to camel case this plural value to match the GraphQL object name. string graphqlObjectName = GraphQLNaming.GenerateListQueryName(entityName, entity); // In case any primitive column names are present, execute the query diff --git a/src/Service/HealthCheck/Utilities.cs b/src/Service/HealthCheck/Utilities.cs index 4b61346736..290410291e 100644 --- a/src/Service/HealthCheck/Utilities.cs +++ b/src/Service/HealthCheck/Utilities.cs @@ -1,9 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; +using System.Data.Common; using System.Text.Json; using Azure.DataApiBuilder.Config.ObjectModel; +using Microsoft.Data.SqlClient; +using Npgsql; namespace Azure.DataApiBuilder.Service.HealthCheck { @@ -32,6 +36,20 @@ public static string GetDatSourceQuery(DatabaseType dbType) } } + public static DbProviderFactory GetDbProviderFactory(DatabaseType dbType) + { + switch (dbType) + { + case DatabaseType.PostgreSQL: + return NpgsqlFactory.Instance; + case DatabaseType.MSSQL: + case DatabaseType.DWSQL: + return SqlClientFactory.Instance; + default: + throw new NotSupportedException($"Database type '{dbType}' is not supported."); + } + } + public static string CreateHttpGraphQLQuery(string entityName, List columnNames, int first) { var payload = new diff --git a/src/Service/Properties/launchSettings.json b/src/Service/Properties/launchSettings.json index acd7ab6646..becf6cd238 100644 --- a/src/Service/Properties/launchSettings.json +++ b/src/Service/Properties/launchSettings.json @@ -1,22 +1,6 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:35704", - "sslPort": 44353 - } - }, "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "graphql", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, "Azure.DataApiBuilder.Service": { "commandName": "Project", "launchBrowser": true, @@ -24,7 +8,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "MsSql" }, - "dotnetRunMessages": "true", + "dotnetRunMessages": true, "applicationUrl": "https://localhost:5001;http://localhost:5000" }, "Development": { @@ -34,7 +18,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "dotnetRunMessages": "true", + "dotnetRunMessages": true, "applicationUrl": "https://localhost:5001;http://localhost:5000" }, "PostgreSql": { @@ -44,7 +28,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "PostgreSql" }, - "dotnetRunMessages": "true", + "dotnetRunMessages": true, "applicationUrl": "https://localhost:5001;http://localhost:5000" }, "MsSql": { @@ -54,7 +38,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "MsSql" }, - "dotnetRunMessages": "true", + "dotnetRunMessages": true, "applicationUrl": "https://localhost:5001;http://localhost:5000" }, "MySql": { @@ -64,7 +48,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "MySql" }, - "dotnetRunMessages": "true", + "dotnetRunMessages": true, "applicationUrl": "https://localhost:5001;http://localhost:5000" }, "CosmosDb_NoSql": { diff --git a/src/Service/dab-config.json b/src/Service/dab-config.json new file mode 100644 index 0000000000..859267100e --- /dev/null +++ b/src/Service/dab-config.json @@ -0,0 +1,231 @@ +{ + "data-source": { + "database-type": "@env('db-type')", + "connection-string": "@env('ConnectionStrings__Database')", + "options": { + "set-session-context": false + } + }, + "runtime": { + "rest": { + "enabled": true, + "path": "/api", + "request-body-strict": true + }, + "graphql": { + "enabled": true, + "path": "/graphql", + "allow-introspection": true + }, + "host": { + "authentication": { + "provider": "StaticWebApps" + }, + "cors": { + "origins": [], + "allow-credentials": false + }, + "mode": "development" + }, + "telemetry": { + "open-telemetry": { + "enabled": true, + "endpoint": "@env('OTEL_EXPORTER_OTLP_ENDPOINT')", + "headers": "@env('OTEL_EXPORTER_OTLP_HEADERS')", + "exporter-protocol": "@env('OTEL_EXPORTER_OTLP_PROTOCOL')", + "service-name": "@env('OTEL_SERVICE_NAME')" + } + }, + "cache": { + "enabled": true, + "ttl-seconds": 60 + } + }, + "entities": { + "Actor": { + "source": { + "object": "Actor", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "Actor", + "plural": "Actors" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + "*" + ] + } + ], + "relationships": { + "character": { + "cardinality": "many", + "target.entity": "Character", + "source.fields": [ + "Id" + ], + "target.fields": [ + "ActorId" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "Character": { + "source": { + "object": "Character", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "Character", + "plural": "Characters" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "actor": { + "cardinality": "one", + "target.entity": "Actor", + "source.fields": [ + "ActorId" + ], + "target.fields": [ + "Id" + ], + "linking.source.fields": [], + "linking.target.fields": [] + }, + "series": { + "cardinality": "many", + "target.entity": "Series", + "source.fields": [ + "Id" + ], + "target.fields": [ + "Id" + ], + "linking.object": "series_character", + "linking.source.fields": [ + "CharacterId" + ], + "linking.target.fields": [ + "SeriesId" + ] + } + } + }, + "Series": { + "source": { + "object": "Series", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "Series", + "plural": "Series" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "character": { + "cardinality": "many", + "target.entity": "Character", + "source.fields": [ + "Id" + ], + "target.fields": [ + "Id" + ], + "linking.object": "series_character", + "linking.source.fields": [ + "SeriesId" + ], + "linking.target.fields": [ + "CharacterId" + ] + } + } + }, + "Species": { + "source": { + "object": "Species", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "Species", + "plural": "Species" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "character": { + "cardinality": "many", + "target.entity": "Character", + "source.fields": [ + "Id" + ], + "target.fields": [ + "Id" + ], + "linking.object": "character_species", + "linking.source.fields": [ + "SpeciesId" + ], + "linking.target.fields": [ + "CharacterId" + ] + } + } + } + } + }