diff --git a/database/003-AddWorkshops.sql b/database/003-AddWorkshops.sql new file mode 100644 index 0000000..3a6ea3c --- /dev/null +++ b/database/003-AddWorkshops.sql @@ -0,0 +1,17 @@ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[Workshops]( + [Id] [uniqueidentifier] NOT NULL, + [Name] [nvarchar](max) NOT NULL, + [Description] [nvarchar](max) NOT NULL, + [StartDate] [datetimeoffset](7) NOT NULL, + [EndDate] [datetimeoffset](7) NOT NULL, + [Created] [datetimeoffset](7) NOT NULL, + CONSTRAINT [PK_Workshops] PRIMARY KEY CLUSTERED +( + [Id] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO \ No newline at end of file diff --git a/database/yobo.schema b/database/yobo.schema index 4cca948..d4c60a1 100644 --- a/database/yobo.schema +++ b/database/yobo.schema @@ -1 +1 @@ -{"Columns@":[{"Key":"dbo.Lessons","Value":{"serializedData":[{"key":"Created","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"Created","TypeInfo@":{"value":"datetimeoffset"},"TypeMapping@":{"ClrType@":"System.DateTimeOffset","DbType@":27,"ProviderTypeName@":{"value":"datetimeoffset"},"ProviderType@":{"value":34}}}},{"key":"Description","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"Description","TypeInfo@":{"value":"nvarchar(-1)"},"TypeMapping@":{"ClrType@":"System.String","DbType@":16,"ProviderTypeName@":{"value":"nvarchar"},"ProviderType@":{"value":12}}}},{"key":"EndDate","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"EndDate","TypeInfo@":{"value":"datetimeoffset"},"TypeMapping@":{"ClrType@":"System.DateTimeOffset","DbType@":27,"ProviderTypeName@":{"value":"datetimeoffset"},"ProviderType@":{"value":34}}}},{"key":"Id","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":true,"Name@":"Id","TypeInfo@":{"value":"uniqueidentifier"},"TypeMapping@":{"ClrType@":"System.Guid","DbType@":9,"ProviderTypeName@":{"value":"uniqueidentifier"},"ProviderType@":{"value":14}}}},{"key":"IsCancelled","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"IsCancelled","TypeInfo@":{"value":"bit"},"TypeMapping@":{"ClrType@":"System.Boolean","DbType@":3,"ProviderTypeName@":{"value":"bit"},"ProviderType@":{"value":2}}}},{"key":"Name","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"Name","TypeInfo@":{"value":"nvarchar(-1)"},"TypeMapping@":{"ClrType@":"System.String","DbType@":16,"ProviderTypeName@":{"value":"nvarchar"},"ProviderType@":{"value":12}}}},{"key":"StartDate","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"StartDate","TypeInfo@":{"value":"datetimeoffset"},"TypeMapping@":{"ClrType@":"System.DateTimeOffset","DbType@":27,"ProviderTypeName@":{"value":"datetimeoffset"},"ProviderType@":{"value":34}}}}]}},{"Key":"dbo.LessonReservations","Value":{"serializedData":[{"key":"Count","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"Count","TypeInfo@":{"value":"int"},"TypeMapping@":{"ClrType@":"System.Int32","DbType@":11,"ProviderTypeName@":{"value":"int"},"ProviderType@":{"value":8}}}},{"key":"Created","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"Created","TypeInfo@":{"value":"datetimeoffset"},"TypeMapping@":{"ClrType@":"System.DateTimeOffset","DbType@":27,"ProviderTypeName@":{"value":"datetimeoffset"},"ProviderType@":{"value":34}}}},{"key":"LessonId","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":true,"Name@":"LessonId","TypeInfo@":{"value":"uniqueidentifier"},"TypeMapping@":{"ClrType@":"System.Guid","DbType@":9,"ProviderTypeName@":{"value":"uniqueidentifier"},"ProviderType@":{"value":14}}}},{"key":"UseCredits","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"UseCredits","TypeInfo@":{"value":"bit"},"TypeMapping@":{"ClrType@":"System.Boolean","DbType@":3,"ProviderTypeName@":{"value":"bit"},"ProviderType@":{"value":2}}}},{"key":"UserId","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":true,"Name@":"UserId","TypeInfo@":{"value":"uniqueidentifier"},"TypeMapping@":{"ClrType@":"System.Guid","DbType@":9,"ProviderTypeName@":{"value":"uniqueidentifier"},"ProviderType@":{"value":14}}}}]}},{"Key":"dbo.Users","Value":{"serializedData":[{"key":"Activated","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":true,"IsPrimaryKey@":false,"Name@":"Activated","TypeInfo@":{"value":"datetimeoffset"},"TypeMapping@":{"ClrType@":"System.DateTimeOffset","DbType@":27,"ProviderTypeName@":{"value":"datetimeoffset"},"ProviderType@":{"value":34}}}},{"key":"ActivationKey","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"ActivationKey","TypeInfo@":{"value":"uniqueidentifier"},"TypeMapping@":{"ClrType@":"System.Guid","DbType@":9,"ProviderTypeName@":{"value":"uniqueidentifier"},"ProviderType@":{"value":14}}}},{"key":"CashReservationBlockedUntil","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":true,"IsPrimaryKey@":false,"Name@":"CashReservationBlockedUntil","TypeInfo@":{"value":"datetimeoffset"},"TypeMapping@":{"ClrType@":"System.DateTimeOffset","DbType@":27,"ProviderTypeName@":{"value":"datetimeoffset"},"ProviderType@":{"value":34}}}},{"key":"Credits","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"Credits","TypeInfo@":{"value":"int"},"TypeMapping@":{"ClrType@":"System.Int32","DbType@":11,"ProviderTypeName@":{"value":"int"},"ProviderType@":{"value":8}}}},{"key":"CreditsExpiration","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":true,"IsPrimaryKey@":false,"Name@":"CreditsExpiration","TypeInfo@":{"value":"datetimeoffset"},"TypeMapping@":{"ClrType@":"System.DateTimeOffset","DbType@":27,"ProviderTypeName@":{"value":"datetimeoffset"},"ProviderType@":{"value":34}}}},{"key":"Email","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"Email","TypeInfo@":{"value":"nvarchar(500)"},"TypeMapping@":{"ClrType@":"System.String","DbType@":16,"ProviderTypeName@":{"value":"nvarchar"},"ProviderType@":{"value":12}}}},{"key":"FirstName","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"FirstName","TypeInfo@":{"value":"nvarchar(50)"},"TypeMapping@":{"ClrType@":"System.String","DbType@":16,"ProviderTypeName@":{"value":"nvarchar"},"ProviderType@":{"value":12}}}},{"key":"Id","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":true,"Name@":"Id","TypeInfo@":{"value":"uniqueidentifier"},"TypeMapping@":{"ClrType@":"System.Guid","DbType@":9,"ProviderTypeName@":{"value":"uniqueidentifier"},"ProviderType@":{"value":14}}}},{"key":"LastName","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"LastName","TypeInfo@":{"value":"nvarchar(100)"},"TypeMapping@":{"ClrType@":"System.String","DbType@":16,"ProviderTypeName@":{"value":"nvarchar"},"ProviderType@":{"value":12}}}},{"key":"PasswordHash","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"PasswordHash","TypeInfo@":{"value":"nvarchar(80)"},"TypeMapping@":{"ClrType@":"System.String","DbType@":16,"ProviderTypeName@":{"value":"nvarchar"},"ProviderType@":{"value":12}}}},{"key":"PasswordResetKey","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":true,"IsPrimaryKey@":false,"Name@":"PasswordResetKey","TypeInfo@":{"value":"uniqueidentifier"},"TypeMapping@":{"ClrType@":"System.Guid","DbType@":9,"ProviderTypeName@":{"value":"uniqueidentifier"},"ProviderType@":{"value":14}}}},{"key":"Registered","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"Registered","TypeInfo@":{"value":"datetimeoffset"},"TypeMapping@":{"ClrType@":"System.DateTimeOffset","DbType@":27,"ProviderTypeName@":{"value":"datetimeoffset"},"ProviderType@":{"value":34}}}}]}}],"Individuals@":[],"IsOffline@":true,"Packages@":[],"PrimaryKeys@":[{"Key":"dbo.Lessons","Value":{"head":"Id","tail":{"head":null,"tail":null}}},{"Key":"dbo.LessonReservations","Value":{"head":"LessonId","tail":{"head":"UserId","tail":{"head":null,"tail":null}}}},{"Key":"dbo.Users","Value":{"head":"Id","tail":{"head":null,"tail":null}}}],"Relationships@":[{"Key":"dbo.Lessons","Value":{"m_Item1":{"head":{"ForeignKey@":"LessonId","ForeignTable@":"dbo.LessonReservations","Name@":"FK_LessonReservations_Lessons","PrimaryKey@":"Id","PrimaryTable@":"dbo.Lessons"},"tail":{"head":null,"tail":null}},"m_Item2":{"head":null,"tail":null}}},{"Key":"dbo.LessonReservations","Value":{"m_Item1":{"head":null,"tail":null},"m_Item2":{"head":{"ForeignKey@":"LessonId","ForeignTable@":"dbo.LessonReservations","Name@":"FK_LessonReservations_Lessons","PrimaryKey@":"Id","PrimaryTable@":"dbo.Lessons"},"tail":{"head":{"ForeignKey@":"UserId","ForeignTable@":"dbo.LessonReservations","Name@":"FK_LessonReservations_Users","PrimaryKey@":"Id","PrimaryTable@":"dbo.Users"},"tail":{"head":null,"tail":null}}}}},{"Key":"dbo.Users","Value":{"m_Item1":{"head":{"ForeignKey@":"UserId","ForeignTable@":"dbo.LessonReservations","Name@":"FK_LessonReservations_Users","PrimaryKey@":"Id","PrimaryTable@":"dbo.Users"},"tail":{"head":null,"tail":null}},"m_Item2":{"head":null,"tail":null}}}],"SprocsParams@":[],"Sprocs@":[],"Tables@":[{"Key":"dbo.Lessons","Value":{"Name@":"Lessons","Schema@":"dbo","Type@":"base table"}},{"Key":"dbo.LessonReservations","Value":{"Name@":"LessonReservations","Schema@":"dbo","Type@":"base table"}},{"Key":"dbo.Users","Value":{"Name@":"Users","Schema@":"dbo","Type@":"base table"}},{"Key":"dbo._SchemaVersions","Value":{"Name@":"_SchemaVersions","Schema@":"dbo","Type@":"base table"}}]} \ No newline at end of file +{"Columns@":[{"Key":"dbo.Lessons","Value":{"serializedData":[{"key":"Created","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"Created","TypeInfo@":{"value":"datetimeoffset"},"TypeMapping@":{"ClrType@":"System.DateTimeOffset","DbType@":27,"ProviderTypeName@":{"value":"datetimeoffset"},"ProviderType@":{"value":34}}}},{"key":"Description","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"Description","TypeInfo@":{"value":"nvarchar(-1)"},"TypeMapping@":{"ClrType@":"System.String","DbType@":16,"ProviderTypeName@":{"value":"nvarchar"},"ProviderType@":{"value":12}}}},{"key":"EndDate","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"EndDate","TypeInfo@":{"value":"datetimeoffset"},"TypeMapping@":{"ClrType@":"System.DateTimeOffset","DbType@":27,"ProviderTypeName@":{"value":"datetimeoffset"},"ProviderType@":{"value":34}}}},{"key":"Id","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":true,"Name@":"Id","TypeInfo@":{"value":"uniqueidentifier"},"TypeMapping@":{"ClrType@":"System.Guid","DbType@":9,"ProviderTypeName@":{"value":"uniqueidentifier"},"ProviderType@":{"value":14}}}},{"key":"IsCancelled","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"IsCancelled","TypeInfo@":{"value":"bit"},"TypeMapping@":{"ClrType@":"System.Boolean","DbType@":3,"ProviderTypeName@":{"value":"bit"},"ProviderType@":{"value":2}}}},{"key":"Name","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"Name","TypeInfo@":{"value":"nvarchar(-1)"},"TypeMapping@":{"ClrType@":"System.String","DbType@":16,"ProviderTypeName@":{"value":"nvarchar"},"ProviderType@":{"value":12}}}},{"key":"StartDate","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"StartDate","TypeInfo@":{"value":"datetimeoffset"},"TypeMapping@":{"ClrType@":"System.DateTimeOffset","DbType@":27,"ProviderTypeName@":{"value":"datetimeoffset"},"ProviderType@":{"value":34}}}}]}},{"Key":"dbo.LessonReservations","Value":{"serializedData":[{"key":"Count","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"Count","TypeInfo@":{"value":"int"},"TypeMapping@":{"ClrType@":"System.Int32","DbType@":11,"ProviderTypeName@":{"value":"int"},"ProviderType@":{"value":8}}}},{"key":"Created","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"Created","TypeInfo@":{"value":"datetimeoffset"},"TypeMapping@":{"ClrType@":"System.DateTimeOffset","DbType@":27,"ProviderTypeName@":{"value":"datetimeoffset"},"ProviderType@":{"value":34}}}},{"key":"LessonId","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":true,"Name@":"LessonId","TypeInfo@":{"value":"uniqueidentifier"},"TypeMapping@":{"ClrType@":"System.Guid","DbType@":9,"ProviderTypeName@":{"value":"uniqueidentifier"},"ProviderType@":{"value":14}}}},{"key":"UseCredits","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"UseCredits","TypeInfo@":{"value":"bit"},"TypeMapping@":{"ClrType@":"System.Boolean","DbType@":3,"ProviderTypeName@":{"value":"bit"},"ProviderType@":{"value":2}}}},{"key":"UserId","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":true,"Name@":"UserId","TypeInfo@":{"value":"uniqueidentifier"},"TypeMapping@":{"ClrType@":"System.Guid","DbType@":9,"ProviderTypeName@":{"value":"uniqueidentifier"},"ProviderType@":{"value":14}}}}]}},{"Key":"dbo.Users","Value":{"serializedData":[{"key":"Activated","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":true,"IsPrimaryKey@":false,"Name@":"Activated","TypeInfo@":{"value":"datetimeoffset"},"TypeMapping@":{"ClrType@":"System.DateTimeOffset","DbType@":27,"ProviderTypeName@":{"value":"datetimeoffset"},"ProviderType@":{"value":34}}}},{"key":"ActivationKey","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"ActivationKey","TypeInfo@":{"value":"uniqueidentifier"},"TypeMapping@":{"ClrType@":"System.Guid","DbType@":9,"ProviderTypeName@":{"value":"uniqueidentifier"},"ProviderType@":{"value":14}}}},{"key":"CashReservationBlockedUntil","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":true,"IsPrimaryKey@":false,"Name@":"CashReservationBlockedUntil","TypeInfo@":{"value":"datetimeoffset"},"TypeMapping@":{"ClrType@":"System.DateTimeOffset","DbType@":27,"ProviderTypeName@":{"value":"datetimeoffset"},"ProviderType@":{"value":34}}}},{"key":"Credits","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"Credits","TypeInfo@":{"value":"int"},"TypeMapping@":{"ClrType@":"System.Int32","DbType@":11,"ProviderTypeName@":{"value":"int"},"ProviderType@":{"value":8}}}},{"key":"CreditsExpiration","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":true,"IsPrimaryKey@":false,"Name@":"CreditsExpiration","TypeInfo@":{"value":"datetimeoffset"},"TypeMapping@":{"ClrType@":"System.DateTimeOffset","DbType@":27,"ProviderTypeName@":{"value":"datetimeoffset"},"ProviderType@":{"value":34}}}},{"key":"Email","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"Email","TypeInfo@":{"value":"nvarchar(500)"},"TypeMapping@":{"ClrType@":"System.String","DbType@":16,"ProviderTypeName@":{"value":"nvarchar"},"ProviderType@":{"value":12}}}},{"key":"FirstName","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"FirstName","TypeInfo@":{"value":"nvarchar(50)"},"TypeMapping@":{"ClrType@":"System.String","DbType@":16,"ProviderTypeName@":{"value":"nvarchar"},"ProviderType@":{"value":12}}}},{"key":"Id","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":true,"Name@":"Id","TypeInfo@":{"value":"uniqueidentifier"},"TypeMapping@":{"ClrType@":"System.Guid","DbType@":9,"ProviderTypeName@":{"value":"uniqueidentifier"},"ProviderType@":{"value":14}}}},{"key":"LastName","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"LastName","TypeInfo@":{"value":"nvarchar(100)"},"TypeMapping@":{"ClrType@":"System.String","DbType@":16,"ProviderTypeName@":{"value":"nvarchar"},"ProviderType@":{"value":12}}}},{"key":"PasswordHash","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"PasswordHash","TypeInfo@":{"value":"nvarchar(80)"},"TypeMapping@":{"ClrType@":"System.String","DbType@":16,"ProviderTypeName@":{"value":"nvarchar"},"ProviderType@":{"value":12}}}},{"key":"PasswordResetKey","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":true,"IsPrimaryKey@":false,"Name@":"PasswordResetKey","TypeInfo@":{"value":"uniqueidentifier"},"TypeMapping@":{"ClrType@":"System.Guid","DbType@":9,"ProviderTypeName@":{"value":"uniqueidentifier"},"ProviderType@":{"value":14}}}},{"key":"Registered","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"Registered","TypeInfo@":{"value":"datetimeoffset"},"TypeMapping@":{"ClrType@":"System.DateTimeOffset","DbType@":27,"ProviderTypeName@":{"value":"datetimeoffset"},"ProviderType@":{"value":34}}}}]}}],"Individuals@":[],"IsOffline@":true,"Packages@":[],"PrimaryKeys@":[{"Key":"dbo.Lessons","Value":{"head":"Id","tail":{"head":null,"tail":null}}},{"Key":"dbo.LessonReservations","Value":{"head":"LessonId","tail":{"head":"UserId","tail":{"head":null,"tail":null}}}},{"Key":"dbo.Users","Value":{"head":"Id","tail":{"head":null,"tail":null}}}],"Relationships@":[{"Key":"dbo.Lessons","Value":{"m_Item1":{"head":{"ForeignKey@":"LessonId","ForeignTable@":"dbo.LessonReservations","Name@":"FK_LessonReservations_Lessons","PrimaryKey@":"Id","PrimaryTable@":"dbo.Lessons"},"tail":{"head":null,"tail":null}},"m_Item2":{"head":null,"tail":null}}},{"Key":"dbo.LessonReservations","Value":{"m_Item1":{"head":null,"tail":null},"m_Item2":{"head":{"ForeignKey@":"LessonId","ForeignTable@":"dbo.LessonReservations","Name@":"FK_LessonReservations_Lessons","PrimaryKey@":"Id","PrimaryTable@":"dbo.Lessons"},"tail":{"head":{"ForeignKey@":"UserId","ForeignTable@":"dbo.LessonReservations","Name@":"FK_LessonReservations_Users","PrimaryKey@":"Id","PrimaryTable@":"dbo.Users"},"tail":{"head":null,"tail":null}}}}},{"Key":"dbo.Users","Value":{"m_Item1":{"head":{"ForeignKey@":"UserId","ForeignTable@":"dbo.LessonReservations","Name@":"FK_LessonReservations_Users","PrimaryKey@":"Id","PrimaryTable@":"dbo.Users"},"tail":{"head":null,"tail":null}},"m_Item2":{"head":null,"tail":null}}}],"SprocsParams@":[],"Sprocs@":[],"Tables@":[{"Key":"dbo.Lessons","Value":{"Name@":"Lessons","Schema@":"dbo","Type@":"base table"}},{"Key":"dbo.LessonReservations","Value":{"Name@":"LessonReservations","Schema@":"dbo","Type@":"base table"}},{"Key":"dbo.Workshops","Value":{"Name@":"Workshops","Schema@":"dbo","Type@":"base table"}},{"Key":"dbo.Users","Value":{"Name@":"Users","Schema@":"dbo","Type@":"base table"}},{"Key":"dbo._SchemaVersions","Value":{"Name@":"_SchemaVersions","Schema@":"dbo","Type@":"base table"}}]} \ No newline at end of file diff --git a/global.json b/global.json index 2be3e86..a4a01b2 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "2.2.105" + "version": "2.2.100" } } \ No newline at end of file diff --git a/src/Yobo.API/Admin/Functions.fs b/src/Yobo.API/Admin/Functions.fs index b2623d9..ff4c56e 100644 --- a/src/Yobo.API/Admin/Functions.fs +++ b/src/Yobo.API/Admin/Functions.fs @@ -34,6 +34,18 @@ module ArgsBuilder = ) Validation.validateAddLesson >> Result.mapError ServerError.ValidationError + let buildAddWorkshop = + ArgsBuilder.build (fun (x:AddWorkshop) -> + ({ + Id = Guid.NewGuid() + StartDate = x.Start + EndDate = x.End + Name = x.Name + Description = x.Description + } : Workshops.CmdArgs.Create) + ) Validation.validateAddWorkshop + >> Result.mapError ServerError.ValidationError + let addCredits cmdHandler (acc:AddCredits) = result { @@ -49,8 +61,21 @@ let addLessons cmdHandler (acc:AddLesson list) = return () } +let addWorkshops cmdHandler (acc:AddWorkshop list) = + result { + let! args = acc |> Result.traverse ArgsBuilder.buildAddWorkshop + let! _ = args |> Result.traverse (Workshops.Command.Create >> CoreCommand.Workshops >> cmdHandler) + return () + } + let cancelLesson cmdHandler (i:Guid) = result { let! _ = ({ Id = i } : Lessons.CmdArgs.Cancel) |> Lessons.Command.Cancel |> CoreCommand.Lessons |> cmdHandler return () + } + +let deleteWorkshop cmdHandler (i:Guid) = + result { + let! _ = ({ Id = i } : Workshops.CmdArgs.Delete) |> Workshops.Command.Delete |> CoreCommand.Workshops |> cmdHandler + return () } \ No newline at end of file diff --git a/src/Yobo.API/CompositionRoot.Communication.fs b/src/Yobo.API/CompositionRoot.Communication.fs index 1266eec..de5d8c1 100644 --- a/src/Yobo.API/CompositionRoot.Communication.fs +++ b/src/Yobo.API/CompositionRoot.Communication.fs @@ -77,8 +77,11 @@ module Admin = GetAllUsers = fun x -> x |> Security.onlyForAdmin snd >>= Services.Users.queries.GetAll |> toAsync AddCredits = fun x -> x |> Security.onlyForAdmin >>= Security.handleForUser addCredits |> toAsync GetLessonsForDateRange = fun x -> x |> Security.onlyForAdmin snd >>= Services.Lessons.queries.GetAllForDateRange |> toAsync + GetWorkshopsForDateRange = fun x -> x |> Security.onlyForAdmin snd >>= Services.Workshops.queries.GetAllForDateRange |> toAsync AddLessons = fun x -> x |> Security.onlyForAdmin >>= Security.handleForUser addLessons |> toAsync + AddWorkshops = fun x -> x |> Security.onlyForAdmin >>= Security.handleForUser addWorkshops |> toAsync CancelLesson = fun x -> x |> Security.onlyForAdmin >>= Security.handleForUser cancelLesson |> toAsync + DeleteWorkshop = fun x -> x |> Security.onlyForAdmin >>= Security.handleForUser deleteWorkshop |> toAsync } module Calendar = diff --git a/src/Yobo.API/CompositionRoot.Services.fs b/src/Yobo.API/CompositionRoot.Services.fs index 20b37a5..38efc6d 100644 --- a/src/Yobo.API/CompositionRoot.Services.fs +++ b/src/Yobo.API/CompositionRoot.Services.fs @@ -48,10 +48,17 @@ module Lessons = |> Lessons.ReadQueries.createDefault |> Lessons.ReadQueries.withError dbErrorToServerError +module Workshops = + let queries = + Configuration.ReadDb.connectionString + |> Workshops.ReadQueries.createDefault + |> Workshops.ReadQueries.withError dbErrorToServerError + // event handlers module EventHandler = open Yobo.Core.Users.EventSerializer open Yobo.Core.Lessons.EventSerializer + open Yobo.Core.Workshops.EventSerializer let private dbHandleFn = DbEventHandler.getHandler Configuration.ReadDb.connectionString let private emailHandleFn = EmailEventHandler.getHandler Users.queries emailService emailSettings @@ -61,6 +68,7 @@ module EventHandler = eventStore.EventAppended.Add(function | LessonsEvent evn -> evn |> CoreEvent.Lessons |> handle + | WorkshopsEvent evn -> evn |> CoreEvent.Workshops |> handle | UsersEvent cryptoProvider evn -> evn |> CoreEvent.Users |> handle | _ -> () ) diff --git a/src/Yobo.Client/Admin/Lessons/Domain.fs b/src/Yobo.Client/Admin/Lessons/Domain.fs index 2f67565..120ac11 100644 --- a/src/Yobo.Client/Admin/Lessons/Domain.fs +++ b/src/Yobo.Client/Admin/Lessons/Domain.fs @@ -4,41 +4,53 @@ open System open Yobo.Shared.Communication open Yobo.Shared.Domain -type State = { - Lessons : Lesson list - WeekOffset : int - SelectedDates : DateTimeOffset list +type AddLessonForm = { StartTime : string EndTime : string Name : string Description : string - FormOpened : bool } with static member Init = { - Lessons = [] - WeekOffset = 0 - SelectedDates = [] StartTime = "" EndTime = "" Name = "" Description = "" + } + +type State = { + Lessons : Lesson list + Workshops : Workshop list + WeekOffset : int + SelectedDates : DateTimeOffset list + FormOpened : bool + AddLessonForm : AddLessonForm +} +with + static member Init = { + Lessons = [] + Workshops = [] + WeekOffset = 0 + SelectedDates = [] FormOpened = false + AddLessonForm = AddLessonForm.Init } type Msg = | Init | LoadLessons | LessonsLoaded of ServerResult + | LoadWorkshops + | WorkshopsLoaded of ServerResult | WeekOffsetChanged of int | DateSelected of DateTimeOffset | DateUnselected of DateTimeOffset - | StartChanged of string - | FormOpened of bool - | EndChanged of string - | NameChanged of string - | DescriptionChanged of string - | SubmitLessonsForm - | LessonsFormSubmitted of ServerResult + | AddLessonFormOpened of bool + | AddLessonFormChanged of AddLessonForm + | SubmitAddLessonForm + | SubmitAddWorkshopForm + | AddLessonFormSubmitted of ServerResult | CancelLesson of Guid - | LessonCancelled of ServerResult \ No newline at end of file + | DeleteWorkshop of Guid + | LessonCancelled of ServerResult + | WorkshopDeleted of ServerResult \ No newline at end of file diff --git a/src/Yobo.Client/Admin/Lessons/State.fs b/src/Yobo.Client/Admin/Lessons/State.fs index 5440aec..6973093 100644 --- a/src/Yobo.Client/Admin/Lessons/State.fs +++ b/src/Yobo.Client/Admin/Lessons/State.fs @@ -29,13 +29,13 @@ let getValidLessonsToAdd (state:State) = |> List.map (fun x -> let st,en = x - |> tryGetFromTo state.StartTime state.EndTime + |> tryGetFromTo state.AddLessonForm.StartTime state.AddLessonForm.EndTime |> Option.defaultValue (DateTimeOffset.MinValue, DateTimeOffset.MinValue) ({ Start = st End = en - Name = state.Name - Description = state.Description + Name = state.AddLessonForm.Name + Description = state.AddLessonForm.Description } : Yobo.Shared.Admin.Domain.AddLesson) ) |> List.filter (fun x -> x.Start <> DateTimeOffset.MinValue) @@ -43,54 +43,90 @@ let getValidLessonsToAdd (state:State) = |> Result.partition |> fst +let getValidWorkshopsToAdd (state:State) = + state.SelectedDates + |> List.map (fun x -> + let st,en = + x + |> tryGetFromTo state.AddLessonForm.StartTime state.AddLessonForm.EndTime + |> Option.defaultValue (DateTimeOffset.MinValue, DateTimeOffset.MinValue) + ({ + Start = st + End = en + Name = state.AddLessonForm.Name + Description = state.AddLessonForm.Description + } : Yobo.Shared.Admin.Domain.AddWorkshop) + ) + |> List.filter (fun x -> x.Start <> DateTimeOffset.MinValue) + |> List.map Yobo.Shared.Admin.Validation.validateAddWorkshop + |> Result.partition + |> fst + let update (msg : Msg) (state : State) : State * Cmd = match msg with - | Init -> state, LoadLessons |> Cmd.ofMsg + | Init -> state, [ LoadLessons; LoadWorkshops] |> List.map Cmd.ofMsg |> Cmd.batch | LoadLessons -> state, state.WeekOffset |> DateRange.getDateRangeForWeekOffset |> SecuredParam.create |> Cmd.ofAsyncResult adminAPI.GetLessonsForDateRange LessonsLoaded + | LoadWorkshops -> + state, + state.WeekOffset + |> DateRange.getDateRangeForWeekOffset + |> SecuredParam.create + |> Cmd.ofAsyncResult adminAPI.GetWorkshopsForDateRange WorkshopsLoaded | LessonsLoaded res -> match res with | Ok less -> { state with Lessons = less}, Cmd.none | Error _ -> state, Cmd.none + | WorkshopsLoaded res -> + match res with + | Ok v -> + { state with Workshops = v}, Cmd.none + | Error _ -> state, Cmd.none | WeekOffsetChanged o -> { state with WeekOffset = o }, LoadLessons |> Cmd.ofMsg | DateSelected d -> { state with SelectedDates = d :: state.SelectedDates }, Cmd.none | DateUnselected d -> let newDates = state.SelectedDates |> List.filter (fun x -> x <> d ) - let newCmd = if newDates.Length > 0 then Cmd.none else (FormOpened(false) |> Cmd.ofMsg) + let newCmd = if newDates.Length > 0 then Cmd.none else (AddLessonFormOpened(false) |> Cmd.ofMsg) { state with SelectedDates = newDates }, newCmd - | StartChanged s -> - { state with StartTime = s }, Cmd.none - | EndChanged s -> - { state with EndTime = s }, Cmd.none - | NameChanged n -> - { state with Name = n }, Cmd.none - | DescriptionChanged n -> - { state with Description = n }, Cmd.none - | FormOpened o -> + | AddLessonFormChanged f -> + { state with AddLessonForm = f }, Cmd.none + | AddLessonFormOpened o -> { state with FormOpened = o }, Cmd.none - | SubmitLessonsForm -> + | SubmitAddLessonForm -> state, (state |> getValidLessonsToAdd |> SecuredParam.create - |> Cmd.ofAsyncResult adminAPI.AddLessons (LessonsFormSubmitted)) - | LessonsFormSubmitted res -> + |> Cmd.ofAsyncResult adminAPI.AddLessons (AddLessonFormSubmitted)) + | SubmitAddWorkshopForm -> + state, + (state + |> getValidWorkshopsToAdd + |> SecuredParam.create + |> Cmd.ofAsyncResult adminAPI.AddWorkshops (AddLessonFormSubmitted)) + | AddLessonFormSubmitted res -> match res with | Ok _ -> { State.Init with WeekOffset = state.WeekOffset }, - [ - SharedView.successToast "Lekce úspěšně přidány." - LoadLessons |> Cmd.ofMsg ] - |> Cmd.batch + [ + SharedView.successToast "Lekce úspěšně přidány." + LoadLessons |> Cmd.ofMsg + LoadWorkshops |> Cmd.ofMsg + ] + |> Cmd.batch | Error e -> state, (e |> SharedView.serverErrorToToast) | CancelLesson id -> state, (id |> SecuredParam.create |> Cmd.ofAsyncResult adminAPI.CancelLesson LessonCancelled) + | DeleteWorkshop id -> + state, (id |> SecuredParam.create |> Cmd.ofAsyncResult adminAPI.DeleteWorkshop WorkshopDeleted) | LessonCancelled res -> state, [ (res |> SharedView.resultToToast "Lekce byla úspěšně zrušena"); LoadLessons |> Cmd.ofMsg ] |> Cmd.batch + | WorkshopDeleted res -> + state, [ (res |> SharedView.resultToToast "Workshop byla úspěšně smazán"); LoadWorkshops |> Cmd.ofMsg ] |> Cmd.batch \ No newline at end of file diff --git a/src/Yobo.Client/Admin/Lessons/View.fs b/src/Yobo.Client/Admin/Lessons/View.fs index 9fc9929..ade55b4 100644 --- a/src/Yobo.Client/Admin/Lessons/View.fs +++ b/src/Yobo.Client/Admin/Lessons/View.fs @@ -15,7 +15,29 @@ module Calendar = open Yobo.Client open Yobo.Shared.Domain - let col dispatch (lessons:Lesson list) (date:DateTimeOffset) = + let col dispatch (lessons:Lesson list) (workshops:Workshop list) (date:DateTimeOffset) = + let workshopDiv (workshop:Workshop) = + let deleteBtn = + if workshop.StartDate > DateTimeOffset.Now then + Button.button [ Button.Color IsDanger; Button.Props [ OnClick (fun _ -> DeleteWorkshop(workshop.Id) |> dispatch) ] ] [ + str "Smazat workshop" + ] + else "Workshop již proběhl" |> str |> SharedView.infoBox + + div [ ClassName "popover is-popover-bottom" ][ + div [ ClassName "popover-trigger lesson" ] [ + div [ ClassName "time" ] [ + workshop.StartDate |> SharedView.toCzTime |> str + str " - " + workshop.EndDate |> SharedView.toCzTime |> str + ] + div [ ClassName "name"] [ str workshop.Name ] + ] + div [ ClassName "popover-content" ] [ + deleteBtn + ] + ] + let lessonDiv (lesson:Lesson) = let cap = if lesson.IsCancelled then "Lekce je zrušena" @@ -65,8 +87,8 @@ module Calendar = td [ ] [ - div [] (lessons |> List.map lessonDiv) + div [] (workshops |> List.map workshopDiv) ] let headerCol (state:State) (dispatch : Msg -> unit) (date:DateTimeOffset) = @@ -103,7 +125,7 @@ module Calendar = let navigation (state:State) dispatch = let addBtn = if state.SelectedDates.Length > 0 then - Button.button [ Button.Color IsPrimary; Button.Props [ OnClick (fun _ -> FormOpened(true) |> dispatch) ] ] [ + Button.button [ Button.Color IsPrimary; Button.Props [ OnClick (fun _ -> AddLessonFormOpened(true) |> dispatch) ] ] [ state.SelectedDates.Length |> sprintf "Přidat %i lekcí" |> str ] else str "" @@ -155,8 +177,8 @@ module Calendar = div [ ClassName "control"] [ Input.text [ Input.Option.Placeholder "Čas začátku lekce, např. 19:00" - Input.Option.Value state.StartTime - Input.Option.OnChange (fun e -> !!e.target?value |> StartChanged |> dispatch) + Input.Option.Value state.AddLessonForm.StartTime + Input.Option.OnChange (fun e -> !!e.target?value |> (fun v -> { state.AddLessonForm with StartTime = v }) |> AddLessonFormChanged |> dispatch) ] ] ] @@ -171,8 +193,8 @@ module Calendar = div [ ClassName "control"] [ Input.text [ Input.Option.Placeholder "Čas konce lekce, např. 20:10" - Input.Option.Value state.EndTime - Input.Option.OnChange (fun e -> !!e.target?value |> EndChanged |> dispatch) + Input.Option.Value state.AddLessonForm.EndTime + Input.Option.OnChange (fun e -> !!e.target?value |> (fun v -> { state.AddLessonForm with EndTime = v }) |> AddLessonFormChanged |> dispatch) ] ] ] @@ -186,8 +208,8 @@ module Calendar = Field.div [ ] [ div [ ClassName "control"] [ Input.text [ - Input.Option.Value state.Name - Input.Option.OnChange (fun e -> !!e.target?value |> NameChanged |> dispatch) + Input.Option.Value state.AddLessonForm.Name + Input.Option.OnChange (fun e -> !!e.target?value |> (fun v -> { state.AddLessonForm with Name = v }) |> AddLessonFormChanged |> dispatch) ] ] ] @@ -201,8 +223,8 @@ module Calendar = Field.div [ ] [ div [ ClassName "control"] [ Textarea.textarea [ - Textarea.Option.Value state.Description - Textarea.Option.OnChange (fun e -> !!e.target?value |> DescriptionChanged |> dispatch) + Textarea.Option.Value state.AddLessonForm.Description + Textarea.Option.OnChange (fun e -> !!e.target?value |> (fun v -> { state.AddLessonForm with Description = v }) |> AddLessonFormChanged |> dispatch) ] [ ] ] ] @@ -218,10 +240,13 @@ module Calendar = Button.button [ Button.Disabled (not isSubmitable) Button.Color IsPrimary - Button.Props [ OnClick (fun _ -> SubmitLessonsForm |> dispatch) ] - ] [ - sprintf "Přidat %i lekcí" (state.SelectedDates.Length) |> str - ] + Button.Props [ OnClick (fun _ -> SubmitAddLessonForm |> dispatch) ] + ] [ sprintf "Přidat %i lekcí" (state.SelectedDates.Length) |> str ] + Button.button [ + Button.Disabled (not isSubmitable) + Button.Color IsInfo + Button.Props [ Style [ MarginLeft 10]; OnClick (fun _ -> SubmitAddWorkshopForm |> dispatch) ] + ] [ sprintf "Přidat jako %i workshopů" (state.SelectedDates.Length) |> str ] ] ] ] @@ -232,7 +257,7 @@ module Calendar = Quickview.quickview [ Quickview.IsActive state.FormOpened ] [ Quickview.header [ ] [ Quickview.title [ ] [ str "Přidat lekce" ] - Delete.delete [ Delete.OnClick (fun _ -> FormOpened(false) |> dispatch) ] [ ] + Delete.delete [ Delete.OnClick (fun _ -> AddLessonFormOpened(false) |> dispatch) ] [ ] ] Quickview.body [ ] [ content ] @@ -245,6 +270,9 @@ module Calendar = let getLessonsForDate (date:DateTimeOffset) = state.Lessons |> List.filter (fun x -> x.StartDate.Date = date.Date) + let getWorkshopsForDate (date:DateTimeOffset) = + state.Workshops + |> List.filter (fun x -> x.StartDate.Date = date.Date) let headerRow = dates @@ -255,7 +283,8 @@ module Calendar = dates |> List.map (fun x -> let lsns = x |> getLessonsForDate - col dispatch lsns x + let wrksps = x |> getWorkshopsForDate + col dispatch lsns wrksps x ) |> tr [ ClassName "day" ] diff --git a/src/Yobo.Client/Calendar/View.fs b/src/Yobo.Client/Calendar/View.fs index 1e68299..778b362 100644 --- a/src/Yobo.Client/Calendar/View.fs +++ b/src/Yobo.Client/Calendar/View.fs @@ -175,7 +175,6 @@ module Calendar = yield headerRow yield row ] - let render user (state : State) (dispatch : Msg -> unit) = div [] [ diff --git a/src/Yobo.Core/CQRS.fs b/src/Yobo.Core/CQRS.fs index bf30f13..f9d96dd 100644 --- a/src/Yobo.Core/CQRS.fs +++ b/src/Yobo.Core/CQRS.fs @@ -6,11 +6,13 @@ type CoreCommand = | UsersRegistry of Users.Registry.Command | Users of Users.Command | Lessons of Lessons.Command + | Workshops of Workshops.Command type CoreEvent = | UsersRegistry of Users.Registry.Event | Users of Users.Event | Lessons of Lessons.Event + | Workshops of Workshops.Event type Saga = | Direct diff --git a/src/Yobo.Core/CommandHandler.fs b/src/Yobo.Core/CommandHandler.fs index eb7d2c2..dd7c41e 100644 --- a/src/Yobo.Core/CommandHandler.fs +++ b/src/Yobo.Core/CommandHandler.fs @@ -59,17 +59,20 @@ let getSagaSetup (cryptoProvider:SymetricCryptoProvider) (eventStore:EventStore) let userRegistryHandler = Users.Registry.CommandHandler.get eventStore let usersHandler = Users.CommandHandler.get cryptoProvider eventStore let lessonsHandler = Lessons.CommandHandler.get eventStore + let workshopsHandler = Workshops.CommandHandler.get eventStore let handle meta corrId cmd = match cmd with | CoreCommand.Users c -> c |> usersHandler.HandleCommand meta corrId List.map CoreEvent.Users | CoreCommand.Lessons c -> c |> lessonsHandler.HandleCommand meta corrId List.map CoreEvent.Lessons + | CoreCommand.Workshops c -> c |> workshopsHandler.HandleCommand meta corrId List.map CoreEvent.Workshops | CoreCommand.UsersRegistry c -> c |> userRegistryHandler.HandleCommand meta corrId List.map CoreEvent.UsersRegistry let compensate meta corrId evn = (match evn with | CoreEvent.Users c -> c |> usersHandler.CompensateEvent meta corrId | CoreEvent.Lessons c -> c |> lessonsHandler.CompensateEvent meta corrId + | CoreEvent.Workshops c -> c |> workshopsHandler.CompensateEvent meta corrId | CoreEvent.UsersRegistry c -> c |> userRegistryHandler.CompensateEvent meta corrId ) |> ignore diff --git a/src/Yobo.Core/DbEventHandler.fs b/src/Yobo.Core/DbEventHandler.fs index f4e7266..c31bde7 100644 --- a/src/Yobo.Core/DbEventHandler.fs +++ b/src/Yobo.Core/DbEventHandler.fs @@ -14,5 +14,6 @@ let getHandler (connString:string) = match cmd with | CoreEvent.Users e -> e |> Users.DbEventHandler.handle |> safeExecute ctx | CoreEvent.Lessons e -> e |> Lessons.DbEventHandler.handle |> safeExecute ctx + | CoreEvent.Workshops e -> e |> Workshops.DbEventHandler.handle |> safeExecute ctx | CoreEvent.UsersRegistry _ -> Ok () handle \ No newline at end of file diff --git a/src/Yobo.Core/EmailEventHandler.fs b/src/Yobo.Core/EmailEventHandler.fs index 747bcc5..dc0d084 100644 --- a/src/Yobo.Core/EmailEventHandler.fs +++ b/src/Yobo.Core/EmailEventHandler.fs @@ -13,5 +13,6 @@ let getHandler (userQueries:Users.ReadQueries.UserQueries<_>) (sender:EmailProvi match cmd with | CoreEvent.Users e -> e |> Users.EmailEventHandler.handle userQueries settings |> send | CoreEvent.Lessons _ + | CoreEvent.Workshops _ | CoreEvent.UsersRegistry _ -> Ok () handle \ No newline at end of file diff --git a/src/Yobo.Core/Workshops/Aggregate.fs b/src/Yobo.Core/Workshops/Aggregate.fs new file mode 100644 index 0000000..7b6c995 --- /dev/null +++ b/src/Yobo.Core/Workshops/Aggregate.fs @@ -0,0 +1,33 @@ +module Yobo.Core.Workshops.Aggregate + +open FSharp.Rop +open Yobo.Shared.Domain +open Yobo.Core.Workshops +open System + +let private onlyIfDoesNotExist state = + if state.Id = State.Init.Id then Ok state + else DomainError.ItemAlreadyExists "Id" |> Error + +let private onlyIfDoesExist state = + if state.Id <> State.Init.Id then Ok state + else DomainError.ItemDoesNotExist "Id" |> Error + +let private onlyIfNotAlreadyDeleted state = + if state.IsDeleted then DomainError.WorkshopIsAlreadyDeleted |> Error + else Ok state + +let execute (state:State) = function + | Create args -> + onlyIfDoesNotExist state + (fun _ -> Created args) + List.singleton + | Delete args -> + onlyIfDoesExist state + >>= onlyIfNotAlreadyDeleted + (fun _ -> Deleted args) + List.singleton + +let apply (state:State) = function + | Created args -> { state with Id = args.Id; StartDate = args.StartDate; EndDate = args.EndDate } + | Deleted _ -> { state with IsDeleted = true } diff --git a/src/Yobo.Core/Workshops/CommandHandler.fs b/src/Yobo.Core/Workshops/CommandHandler.fs new file mode 100644 index 0000000..eda687b --- /dev/null +++ b/src/Yobo.Core/Workshops/CommandHandler.fs @@ -0,0 +1,33 @@ +module Yobo.Core.Workshops.CommandHandler + +open Yobo.Core.EventStoreCommandHandler + +let private getIdFromCmd = function + | Create args -> args |> Extractor.getIdFromCommand + | Delete args -> args |> Extractor.getIdFromCommand + +let private getIdFromEvn = function + | Created args -> args |> Extractor.getIdFromCommand + | Deleted args -> args |> Extractor.getIdFromCommand + + +let private settings = { + Aggregate = { + Init = State.Init + Execute = Aggregate.execute + Apply = Aggregate.apply + } + StreamIdReader = { + FromCommand = getIdFromCmd >> sprintf "%s%s" EventSerializer.streamPrefix + FromEvent = getIdFromEvn >> sprintf "%s%s" EventSerializer.streamPrefix + } + Serializer = { + EventToData = EventSerializer.toData + DataToEvent = EventSerializer.toEvent + } + Validators = [] + TryGetRollbackEvent = fun _ _ -> None +} + +let get store = + store |> getCommandHandler settings \ No newline at end of file diff --git a/src/Yobo.Core/Workshops/DbEventHandler.fs b/src/Yobo.Core/Workshops/DbEventHandler.fs new file mode 100644 index 0000000..d9e0a66 --- /dev/null +++ b/src/Yobo.Core/Workshops/DbEventHandler.fs @@ -0,0 +1,5 @@ +module Yobo.Core.Workshops.DbEventHandler + +let handle = function + | Created args -> UpdateQueries.created args + | Deleted args -> UpdateQueries.deleted args diff --git a/src/Yobo.Core/Workshops/EventSerializer.fs b/src/Yobo.Core/Workshops/EventSerializer.fs new file mode 100644 index 0000000..b87bca1 --- /dev/null +++ b/src/Yobo.Core/Workshops/EventSerializer.fs @@ -0,0 +1,19 @@ +module Yobo.Core.Workshops.EventSerializer + +open Yobo.Core + +let toEvent = function + | "Created", data -> data |> Serialization.objectFromJToken |> Created + | "Deleted", data -> data |> Serialization.objectFromJToken |> Deleted + | n,_ -> failwithf "Unrecognized event %s" n + +let toData = function + | Created args -> "Created", (args |> Serialization.objectToJToken) + | Deleted args -> "Deleted", (args |> Serialization.objectToJToken) + +let streamPrefix = "Workshops-" + +let (|WorkshopsEvent|_|) (event:CosmoStore.EventRead) = + if event.StreamId.StartsWith(streamPrefix) then + toEvent (event.Name, event.Data) |> Some + else None \ No newline at end of file diff --git a/src/Yobo.Core/Workshops/ReadQueries.fs b/src/Yobo.Core/Workshops/ReadQueries.fs new file mode 100644 index 0000000..1363d38 --- /dev/null +++ b/src/Yobo.Core/Workshops/ReadQueries.fs @@ -0,0 +1,40 @@ +module Yobo.Core.Workshops.ReadQueries + +open Yobo.Core +open System +open FSharp.Rop +open Yobo.Shared.Domain +open Extensions + +type WorkshopsQueries<'a> = { + GetAllForDateRange : (DateTimeOffset * DateTimeOffset) -> Result +} + +let withError (fn:'a -> 'b) (q:WorkshopsQueries<'a>) = { + GetAllForDateRange = q.GetAllForDateRange >> Result.mapError fn +} + +let internal workshopFromDbEntity (u:ReadDb.Db.dataContext.``dbo.WorkshopsEntity``) = + { + Id = u.Id + Name = u.Name + Description = u.Description + StartDate = u.StartDate.ToCzDateTimeOffset() + EndDate = u.EndDate.ToCzDateTimeOffset() + } + +let private getAllForDateRange (st, en) (ctx:ReadDb.Db.dataContext) = + query { + for x in ctx.Dbo.Workshops do + where (x.StartDate >= st && x.StartDate <= en) + sortBy x.StartDate + select x + } + |> Seq.toList + |> List.map workshopFromDbEntity + +let createDefault (connString:string) = + let ctx = ReadDb.Db.GetDataContext(connString) + { + GetAllForDateRange = getAllForDateRange >> Data.tryQuery ctx + } \ No newline at end of file diff --git a/src/Yobo.Core/Workshops/UpdateQueries.fs b/src/Yobo.Core/Workshops/UpdateQueries.fs new file mode 100644 index 0000000..a869f48 --- /dev/null +++ b/src/Yobo.Core/Workshops/UpdateQueries.fs @@ -0,0 +1,27 @@ +module Yobo.Core.Workshops.UpdateQueries + +open Yobo.Core +open System + +let private getById (ctx:ReadDb.Db.dataContext) i = + query { + for x in ctx.Dbo.Workshops do + where (x.Id = i) + select x + } |> Seq.head + + +let created (args:CmdArgs.Create) (ctx:ReadDb.Db.dataContext) = + let item = ctx.Dbo.Workshops.Create() + item.Id <- args.Id + item.Name <- args.Name + item.Description <- args.Description + item.StartDate <- args.StartDate + item.EndDate <- args.EndDate + item.Created <- DateTimeOffset.Now + ctx.SubmitUpdates() + +let deleted (args:CmdArgs.Delete) (ctx:ReadDb.Db.dataContext) = + let item = args.Id |> getById ctx + item.Delete() + ctx.SubmitUpdates() \ No newline at end of file diff --git a/src/Yobo.Core/Workshops/Workshops.fs b/src/Yobo.Core/Workshops/Workshops.fs new file mode 100644 index 0000000..236e1b2 --- /dev/null +++ b/src/Yobo.Core/Workshops/Workshops.fs @@ -0,0 +1,39 @@ +namespace Yobo.Core.Workshops + +open System + +module CmdArgs = + + type Create = { + Id : Guid + StartDate : DateTimeOffset + EndDate : DateTimeOffset + Name : string + Description : string + } + + type Delete = { + Id : Guid + } + +type Command = + | Create of CmdArgs.Create + | Delete of CmdArgs.Delete + +type Event = + | Created of CmdArgs.Create + | Deleted of CmdArgs.Delete + +type State = { + Id : Guid + StartDate : DateTimeOffset + EndDate : DateTimeOffset + IsDeleted : bool +} +with + static member Init = { + Id = Guid.Empty + StartDate = DateTimeOffset.MinValue + EndDate = DateTimeOffset.MinValue + IsDeleted = false + } \ No newline at end of file diff --git a/src/Yobo.Core/Yobo.Core.fsproj b/src/Yobo.Core/Yobo.Core.fsproj index 490b363..8cb83ba 100644 --- a/src/Yobo.Core/Yobo.Core.fsproj +++ b/src/Yobo.Core/Yobo.Core.fsproj @@ -48,6 +48,13 @@ + + + + + + + diff --git a/src/Yobo.Shared/Admin/Communication.fs b/src/Yobo.Shared/Admin/Communication.fs index 000ce13..1e771d6 100644 --- a/src/Yobo.Shared/Admin/Communication.fs +++ b/src/Yobo.Shared/Admin/Communication.fs @@ -11,6 +11,9 @@ type API = { GetAllUsers : SecuredParam -> ServerResponse AddCredits : SecuredParam -> ServerResponse GetLessonsForDateRange : SecuredParam -> ServerResponse + GetWorkshopsForDateRange : SecuredParam -> ServerResponse AddLessons : SecuredParam -> ServerResponse + AddWorkshops : SecuredParam -> ServerResponse CancelLesson : SecuredParam -> ServerResponse + DeleteWorkshop : SecuredParam -> ServerResponse } \ No newline at end of file diff --git a/src/Yobo.Shared/Admin/Domain.fs b/src/Yobo.Shared/Admin/Domain.fs index 854fde1..b448575 100644 --- a/src/Yobo.Shared/Admin/Domain.fs +++ b/src/Yobo.Shared/Admin/Domain.fs @@ -15,3 +15,9 @@ type AddLesson = { Description : string } +type AddWorkshop = { + Start : DateTimeOffset + End : DateTimeOffset + Name : string + Description : string +} \ No newline at end of file diff --git a/src/Yobo.Shared/Admin/Validation.fs b/src/Yobo.Shared/Admin/Validation.fs index 600a2f6..3824745 100644 --- a/src/Yobo.Shared/Admin/Validation.fs +++ b/src/Yobo.Shared/Admin/Validation.fs @@ -17,3 +17,10 @@ let validateAddLesson (args:AddLesson) = "Description", validateNotEmpty (fun (x:AddLesson) -> x.Description) "End", validateIsBeforeAnother (fun x -> x.Start) (fun x -> x.End) ] |> validate args + +let validateAddWorkshop (args:AddWorkshop) = + [ + "Name", validateNotEmpty (fun (x:AddWorkshop) -> x.Name) + "Description", validateNotEmpty (fun (x:AddWorkshop) -> x.Description) + "End", validateIsBeforeAnother (fun x -> x.Start) (fun x -> x.End) + ] |> validate args diff --git a/src/Yobo.Shared/Domain.fs b/src/Yobo.Shared/Domain.fs index ad3b239..4ed8f2a 100644 --- a/src/Yobo.Shared/Domain.fs +++ b/src/Yobo.Shared/Domain.fs @@ -17,6 +17,7 @@ type DomainError = | NotEnoughCredits | CashReservationIsBlocked | LessonIsAlreadyCancelled + | WorkshopIsAlreadyDeleted with member x.Explain() = match x with @@ -34,6 +35,7 @@ type DomainError = | NotEnoughCredits -> "Nemáte dostatek kreditů." | CashReservationIsBlocked -> "Rezervaci v hotovosti nelze provést." | LessonIsAlreadyCancelled -> "Lekce je již zrušena." + | WorkshopIsAlreadyDeleted -> "Workshop je již smazán." type User = { Id : Guid @@ -69,4 +71,12 @@ type Lesson = { Description : string Reservations : (User * UserReservation) list IsCancelled : bool +} + +type Workshop = { + Id : Guid + StartDate : DateTimeOffset + EndDate : DateTimeOffset + Name : string + Description : string } \ No newline at end of file