diff --git a/IHP/AuthSupport/Controller/Sessions.hs b/IHP/AuthSupport/Controller/Sessions.hs index 0e18680b9..027d04b47 100644 --- a/IHP/AuthSupport/Controller/Sessions.hs +++ b/IHP/AuthSupport/Controller/Sessions.hs @@ -64,6 +64,7 @@ createSessionAction :: forall record action passwordField. , CanUpdate record , Show (PrimaryKey (GetTableName record)) , record ~ GetModelByTableName (GetTableName record) + , Table record ) => IO () createSessionAction = do usersQueryBuilder diff --git a/IHP/Fetch.hs b/IHP/Fetch.hs index 6dac4dd30..d2276c1f7 100644 --- a/IHP/Fetch.hs +++ b/IHP/Fetch.hs @@ -36,58 +36,58 @@ import IHP.QueryBuilder class Fetchable fetchable model | fetchable -> model where type FetchResult fetchable model - fetch :: (KnownSymbol (GetTableName model), PG.FromRow model, ?modelContext :: ModelContext) => fetchable -> IO (FetchResult fetchable model) - fetchOneOrNothing :: (KnownSymbol (GetTableName model), PG.FromRow model, ?modelContext :: ModelContext) => fetchable -> IO (Maybe model) - fetchOne :: (KnownSymbol (GetTableName model), PG.FromRow model, ?modelContext :: ModelContext) => fetchable -> IO model + fetch :: (Table model, KnownSymbol (GetTableName model), PG.FromRow model, ?modelContext :: ModelContext) => fetchable -> IO (FetchResult fetchable model) + fetchOneOrNothing :: (Table model, KnownSymbol (GetTableName model), PG.FromRow model, ?modelContext :: ModelContext) => fetchable -> IO (Maybe model) + fetchOne :: (Table model, KnownSymbol (GetTableName model), PG.FromRow model, ?modelContext :: ModelContext) => fetchable -> IO model -- The instance declaration had to be split up because a type variable ranging over HasQueryBuilder instances is not allowed in the declaration of the associated type. The common*-functions reduce the redundancy to the necessary minimum. instance (model ~ GetModelByTableName table, KnownSymbol table) => Fetchable (QueryBuilder table) model where type instance FetchResult (QueryBuilder table) model = [model] {-# INLINE fetch #-} - fetch :: (KnownSymbol (GetTableName model), PG.FromRow model, ?modelContext :: ModelContext) => QueryBuilder table -> IO [model] + fetch :: (Table model, KnownSymbol (GetTableName model), PG.FromRow model, ?modelContext :: ModelContext) => QueryBuilder table -> IO [model] fetch = commonFetch {-# INLINE fetchOneOrNothing #-} - fetchOneOrNothing :: (?modelContext :: ModelContext) => (PG.FromRow model, KnownSymbol (GetTableName model)) => QueryBuilder table -> IO (Maybe model) + fetchOneOrNothing :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model, KnownSymbol (GetTableName model)) => QueryBuilder table -> IO (Maybe model) fetchOneOrNothing = commonFetchOneOrNothing {-# INLINE fetchOne #-} - fetchOne :: (?modelContext :: ModelContext) => (PG.FromRow model, KnownSymbol (GetTableName model)) => QueryBuilder table -> IO model + fetchOne :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model, KnownSymbol (GetTableName model)) => QueryBuilder table -> IO model fetchOne = commonFetchOne instance (model ~ GetModelByTableName table, KnownSymbol table) => Fetchable (JoinQueryBuilderWrapper r table) model where type instance FetchResult (JoinQueryBuilderWrapper r table) model = [model] {-# INLINE fetch #-} - fetch :: (KnownSymbol (GetTableName model), PG.FromRow model, ?modelContext :: ModelContext) => JoinQueryBuilderWrapper r table -> IO [model] + fetch :: (Table model, KnownSymbol (GetTableName model), PG.FromRow model, ?modelContext :: ModelContext) => JoinQueryBuilderWrapper r table -> IO [model] fetch = commonFetch {-# INLINE fetchOneOrNothing #-} - fetchOneOrNothing :: (?modelContext :: ModelContext) => (PG.FromRow model, KnownSymbol (GetTableName model)) => JoinQueryBuilderWrapper r table -> IO (Maybe model) + fetchOneOrNothing :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model, KnownSymbol (GetTableName model)) => JoinQueryBuilderWrapper r table -> IO (Maybe model) fetchOneOrNothing = commonFetchOneOrNothing {-# INLINE fetchOne #-} - fetchOne :: (?modelContext :: ModelContext) => (PG.FromRow model, KnownSymbol (GetTableName model)) => JoinQueryBuilderWrapper r table -> IO model + fetchOne :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model, KnownSymbol (GetTableName model)) => JoinQueryBuilderWrapper r table -> IO model fetchOne = commonFetchOne instance (model ~ GetModelByTableName table, KnownSymbol table) => Fetchable (NoJoinQueryBuilderWrapper table) model where type instance FetchResult (NoJoinQueryBuilderWrapper table) model = [model] {-# INLINE fetch #-} - fetch :: (KnownSymbol (GetTableName model), PG.FromRow model, ?modelContext :: ModelContext) => NoJoinQueryBuilderWrapper table -> IO [model] + fetch :: (Table model, KnownSymbol (GetTableName model), PG.FromRow model, ?modelContext :: ModelContext) => NoJoinQueryBuilderWrapper table -> IO [model] fetch = commonFetch {-# INLINE fetchOneOrNothing #-} - fetchOneOrNothing :: (?modelContext :: ModelContext) => (PG.FromRow model, KnownSymbol (GetTableName model)) => NoJoinQueryBuilderWrapper table -> IO (Maybe model) + fetchOneOrNothing :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model, KnownSymbol (GetTableName model)) => NoJoinQueryBuilderWrapper table -> IO (Maybe model) fetchOneOrNothing = commonFetchOneOrNothing {-# INLINE fetchOne #-} - fetchOne :: (?modelContext :: ModelContext) => (PG.FromRow model, KnownSymbol (GetTableName model)) => NoJoinQueryBuilderWrapper table -> IO model + fetchOne :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model, KnownSymbol (GetTableName model)) => NoJoinQueryBuilderWrapper table -> IO model fetchOne = commonFetchOne instance (model ~ GetModelByTableName table, KnownSymbol table, FromField value, KnownSymbol foreignTable, foreignModel ~ GetModelByTableName foreignTable, KnownSymbol columnName, HasField columnName foreignModel value, HasQueryBuilder (LabeledQueryBuilderWrapper foreignTable columnName value) NoJoins) => Fetchable (LabeledQueryBuilderWrapper foreignTable columnName value table) model where type instance FetchResult (LabeledQueryBuilderWrapper foreignTable columnName value table) model = [LabeledData value model] -- fetch needs to return a list of labeled data. The {-# INLINE fetch #-} - fetch :: (KnownSymbol (GetTableName model), PG.FromRow model, ?modelContext :: ModelContext) => LabeledQueryBuilderWrapper foreignTable columnName value table -> IO [LabeledData value model] + fetch :: (Table model, KnownSymbol (GetTableName model), PG.FromRow model, ?modelContext :: ModelContext) => LabeledQueryBuilderWrapper foreignTable columnName value table -> IO [LabeledData value model] fetch !queryBuilderProvider = do let !(theQuery, theParameters) = queryBuilderProvider |> toSQL @@ -95,17 +95,17 @@ instance (model ~ GetModelByTableName table, KnownSymbol table, FromField value, sqlQuery @_ @(LabeledData value model) (Query $ cs theQuery) theParameters {-# INLINE fetchOneOrNothing #-} - fetchOneOrNothing :: (?modelContext :: ModelContext) => (PG.FromRow model, KnownSymbol (GetTableName model)) => LabeledQueryBuilderWrapper foreignTable columnName value table -> IO (Maybe model) + fetchOneOrNothing :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model, KnownSymbol (GetTableName model)) => LabeledQueryBuilderWrapper foreignTable columnName value table -> IO (Maybe model) fetchOneOrNothing = commonFetchOneOrNothing {-# INLINE fetchOne #-} - fetchOne :: (?modelContext :: ModelContext) => (PG.FromRow model, KnownSymbol (GetTableName model)) => LabeledQueryBuilderWrapper foreignTable columnName value table -> IO model + fetchOne :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model, KnownSymbol (GetTableName model)) => LabeledQueryBuilderWrapper foreignTable columnName value table -> IO model fetchOne = commonFetchOne {-# INLINE commonFetch #-} -commonFetch :: forall model table queryBuilderProvider joinRegister.(HasQueryBuilder queryBuilderProvider joinRegister, model ~ GetModelByTableName table, KnownSymbol table, KnownSymbol (GetTableName model), PG.FromRow model, ?modelContext :: ModelContext) => queryBuilderProvider table -> IO [model] +commonFetch :: forall model table queryBuilderProvider joinRegister.(Table model, HasQueryBuilder queryBuilderProvider joinRegister, model ~ GetModelByTableName table, KnownSymbol table, KnownSymbol (GetTableName model), PG.FromRow model, ?modelContext :: ModelContext) => queryBuilderProvider table -> IO [model] commonFetch !queryBuilder = do let !(theQuery, theParameters) = queryBuilder |> toSQL @@ -113,7 +113,7 @@ commonFetch !queryBuilder = do sqlQuery (Query $ cs theQuery) theParameters {-# INLINE commonFetchOneOrNothing #-} -commonFetchOneOrNothing :: forall model table queryBuilderProvider joinRegister.(?modelContext :: ModelContext) => (KnownSymbol table, HasQueryBuilder queryBuilderProvider joinRegister, PG.FromRow model, KnownSymbol (GetTableName model)) => queryBuilderProvider table -> IO (Maybe model) +commonFetchOneOrNothing :: forall model table queryBuilderProvider joinRegister.(?modelContext :: ModelContext) => (Table model, KnownSymbol table, HasQueryBuilder queryBuilderProvider joinRegister, PG.FromRow model, KnownSymbol (GetTableName model)) => queryBuilderProvider table -> IO (Maybe model) commonFetchOneOrNothing !queryBuilder = do let !(theQuery, theParameters) = queryBuilder |> buildQuery @@ -124,7 +124,7 @@ commonFetchOneOrNothing !queryBuilder = do pure $ listToMaybe results {-# INLINE commonFetchOne #-} -commonFetchOne :: forall model table queryBuilderProvider joinRegister.(?modelContext :: ModelContext) => (KnownSymbol table, Fetchable (queryBuilderProvider table) model, HasQueryBuilder queryBuilderProvider joinRegister, PG.FromRow model, KnownSymbol (GetTableName model)) => queryBuilderProvider table -> IO model +commonFetchOne :: forall model table queryBuilderProvider joinRegister.(?modelContext :: ModelContext) => (Table model, KnownSymbol table, Fetchable (queryBuilderProvider table) model, HasQueryBuilder queryBuilderProvider joinRegister, PG.FromRow model, KnownSymbol (GetTableName model)) => queryBuilderProvider table -> IO model commonFetchOne !queryBuilder = do maybeModel <- fetchOneOrNothing queryBuilder case maybeModel of @@ -174,27 +174,27 @@ fetchExists !queryBuilder = do {-# INLINE fetchExists #-} {-# INLINE genericFetchId #-} -genericFetchId :: forall table model. (KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, FilterPrimaryKey table, model ~ GetModelByTableName table, GetTableName model ~ table) => Id' table -> IO [model] +genericFetchId :: forall table model. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, FilterPrimaryKey table, model ~ GetModelByTableName table, GetTableName model ~ table) => Id' table -> IO [model] genericFetchId !id = query @model |> filterWhereId id |> fetch {-# INLINE genericfetchIdOneOrNothing #-} -genericfetchIdOneOrNothing :: forall table model. (KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, FilterPrimaryKey table, model ~ GetModelByTableName table, GetTableName model ~ table) => Id' table -> IO (Maybe model) +genericfetchIdOneOrNothing :: forall table model. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, FilterPrimaryKey table, model ~ GetModelByTableName table, GetTableName model ~ table) => Id' table -> IO (Maybe model) genericfetchIdOneOrNothing !id = query @model |> filterWhereId id |> fetchOneOrNothing {-# INLINE genericFetchIdOne #-} -genericFetchIdOne :: forall table model. (KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, FilterPrimaryKey table, model ~ GetModelByTableName table, GetTableName model ~ table) => Id' table -> IO model +genericFetchIdOne :: forall table model. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, FilterPrimaryKey table, model ~ GetModelByTableName table, GetTableName model ~ table) => Id' table -> IO model genericFetchIdOne !id = query @model |> filterWhereId id |> fetchOne {-# INLINE genericFetchIds #-} -genericFetchIds :: forall table model value. (KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, ToField value, EqOrIsOperator value, HasField "id" model value, model ~ GetModelByTableName table, GetTableName model ~ table) => [value] -> IO [model] +genericFetchIds :: forall table model value. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, ToField value, EqOrIsOperator value, HasField "id" model value, model ~ GetModelByTableName table, GetTableName model ~ table) => [value] -> IO [model] genericFetchIds !ids = query @model |> filterWhereIn (#id, ids) |> fetch {-# INLINE genericfetchIdsOneOrNothing #-} -genericfetchIdsOneOrNothing :: forall model value table. (KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, ToField value, EqOrIsOperator value, HasField "id" model value, model ~ GetModelByTableName table, GetTableName model ~ table) => [value] -> IO (Maybe model) +genericfetchIdsOneOrNothing :: forall model value table. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, ToField value, EqOrIsOperator value, HasField "id" model value, model ~ GetModelByTableName table, GetTableName model ~ table) => [value] -> IO (Maybe model) genericfetchIdsOneOrNothing !ids = query @model |> filterWhereIn (#id, ids) |> fetchOneOrNothing {-# INLINE genericFetchIdsOne #-} -genericFetchIdsOne :: forall model value table. (KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, ToField value, EqOrIsOperator value, HasField "id" model value, model ~ GetModelByTableName table, GetTableName model ~ table) => [value] -> IO model +genericFetchIdsOne :: forall model value table. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, ToField value, EqOrIsOperator value, HasField "id" model value, model ~ GetModelByTableName table, GetTableName model ~ table) => [value] -> IO model genericFetchIdsOne !ids = query @model |> filterWhereIn (#id, ids) |> fetchOne {-# INLINE findBy #-} diff --git a/IHP/FetchRelated.hs b/IHP/FetchRelated.hs index b10be61e8..c93482f52 100644 --- a/IHP/FetchRelated.hs +++ b/IHP/FetchRelated.hs @@ -13,7 +13,7 @@ module IHP.FetchRelated (fetchRelated, collectionFetchRelated, collectionFetchRe import IHP.Prelude import Database.PostgreSQL.Simple.ToField import qualified Database.PostgreSQL.Simple as PG -import IHP.ModelSupport (Include, Id', PrimaryKey, GetModelByTableName) +import IHP.ModelSupport (Include, Id', PrimaryKey, GetModelByTableName, Table) import IHP.QueryBuilder import IHP.Fetch @@ -71,6 +71,7 @@ instance ( , Show (PrimaryKey tableName) , HasField "id" relatedModel (Id' tableName) , relatedModel ~ GetModelByTableName (GetTableName relatedModel) + , Table relatedModel ) => CollectionFetchRelated (Id' tableName) relatedModel where collectionFetchRelated :: forall model relatedField. ( ?modelContext :: ModelContext, @@ -79,7 +80,8 @@ instance ( Fetchable (Id' tableName) relatedModel, KnownSymbol (GetTableName relatedModel), PG.FromRow relatedModel, - KnownSymbol relatedField + KnownSymbol relatedField, + Table relatedModel ) => Proxy relatedField -> [model] -> IO [Include relatedField model] collectionFetchRelated relatedField model = do relatedModels :: [relatedModel] <- query @relatedModel |> filterWhereIn (#id, map (getField @relatedField) model) |> fetch @@ -120,6 +122,7 @@ instance ( , Show (PrimaryKey tableName) , HasField "id" relatedModel (Id' tableName) , relatedModel ~ GetModelByTableName (GetTableName relatedModel) + , Table relatedModel ) => CollectionFetchRelatedOrNothing (Id' tableName) relatedModel where collectionFetchRelatedOrNothing :: forall model relatedField. ( ?modelContext :: ModelContext, @@ -158,7 +161,7 @@ instance ( -- This will query all posts with their comments. The type of @posts@ is @[Include "comments" Post]@. -- -- When fetching query builders, currently the implementation is not very efficient. E.g. given 10 Posts above, it will run 10 queries to fetch the comments. We should optimise this behavior in the future. -instance (relatedModel ~ GetModelByTableName relatedTable) => CollectionFetchRelated (QueryBuilder relatedTable) relatedModel where +instance (relatedModel ~ GetModelByTableName relatedTable, Table relatedModel) => CollectionFetchRelated (QueryBuilder relatedTable) relatedModel where collectionFetchRelated :: forall model relatedField. ( ?modelContext :: ModelContext, HasField relatedField model (QueryBuilder relatedTable), @@ -181,7 +184,8 @@ fetchRelated :: forall model field fieldValue fetchModel. ( HasField field model fieldValue, PG.FromRow fetchModel, KnownSymbol (GetTableName fetchModel), - Fetchable fieldValue fetchModel + Fetchable fieldValue fetchModel, + Table fetchModel ) => Proxy field -> model -> IO (Include field model) fetchRelated relatedField model = do result :: FetchResult fieldValue fetchModel <- fetch ((getField @field model) :: fieldValue) @@ -195,7 +199,8 @@ fetchRelatedOrNothing :: forall model field fieldValue fetchModel. ( HasField field model (Maybe fieldValue), PG.FromRow fetchModel, KnownSymbol (GetTableName fetchModel), - Fetchable fieldValue fetchModel + Fetchable fieldValue fetchModel, + Table fetchModel ) => Proxy field -> model -> IO (Include field model) fetchRelatedOrNothing relatedField model = do result :: Maybe (FetchResult fieldValue fetchModel) <- case getField @field model of @@ -211,7 +216,8 @@ maybeFetchRelatedOrNothing :: forall model field fieldValue fetchModel. ( HasField field model (Maybe fieldValue), PG.FromRow fetchModel, KnownSymbol (GetTableName fetchModel), - Fetchable fieldValue fetchModel + Fetchable fieldValue fetchModel, + Table fetchModel ) => Proxy field -> Maybe model -> IO (Maybe (Include field model)) maybeFetchRelatedOrNothing relatedField = maybe (pure Nothing) (\q -> fetchRelatedOrNothing relatedField q >>= pure . Just) {-# INLINE maybeFetchRelatedOrNothing #-} diff --git a/IHP/Job/Queue.hs b/IHP/Job/Queue.hs index 880ce02b3..89f08134d 100644 --- a/IHP/Job/Queue.hs +++ b/IHP/Job/Queue.hs @@ -39,7 +39,7 @@ fetchNextJob :: forall job. , FromRow job , Show (PrimaryKey (GetTableName job)) , PG.FromField (PrimaryKey (GetTableName job)) - , KnownSymbol (GetTableName job) + , Table job ) => UUID -> IO (Maybe job) fetchNextJob workerId = do let query = "UPDATE ? SET status = ?, locked_at = NOW(), locked_by = ?, attempts_count = attempts_count + 1 WHERE id IN (SELECT id FROM ? WHERE ((status = ?) OR (status = ? AND updated_at < NOW() + interval '30 seconds')) AND locked_by IS NULL AND run_at <= NOW() ORDER BY created_at LIMIT 1 FOR UPDATE) RETURNING id" diff --git a/IHP/Job/Runner.hs b/IHP/Job/Runner.hs index f31d87009..f185cca64 100644 --- a/IHP/Job/Runner.hs +++ b/IHP/Job/Runner.hs @@ -80,6 +80,7 @@ worker :: forall job. , Job job , CanUpdate job , Show job + , Table job ) => JobWorker worker = JobWorker (jobWorkerFetchAndRunLoop @job) @@ -100,6 +101,7 @@ jobWorkerFetchAndRunLoop :: forall job. , Job job , CanUpdate job , Show job + , Table job ) => JobWorkerArgs -> IO (Async.Async ()) jobWorkerFetchAndRunLoop JobWorkerArgs { .. } = do let ?context = frameworkConfig diff --git a/IHP/LoginSupport/Middleware.hs b/IHP/LoginSupport/Middleware.hs index 187cdfb71..a5843b595 100644 --- a/IHP/LoginSupport/Middleware.hs +++ b/IHP/LoginSupport/Middleware.hs @@ -12,16 +12,17 @@ import IHP.ModelSupport import IHP.Controller.Context {-# INLINE initAuthentication #-} -initAuthentication :: forall user. +initAuthentication :: forall user normalizedModel. ( ?context :: ControllerContext , ?modelContext :: ModelContext - , Typeable (NormalizeModel user) - , KnownSymbol (GetTableName (NormalizeModel user)) + , normalizedModel ~ NormalizeModel user + , Typeable normalizedModel + , Table normalizedModel + , FromRow normalizedModel + , PrimaryKey (GetTableName normalizedModel) ~ UUID + , GetTableName normalizedModel ~ GetTableName user + , FilterPrimaryKey (GetTableName normalizedModel) , KnownSymbol (GetModelName user) - , GetTableName (NormalizeModel user) ~ GetTableName user - , FromRow (NormalizeModel user) - , PrimaryKey (GetTableName user) ~ UUID - , FilterPrimaryKey (GetTableName user) ) => IO () initAuthentication = do user <- getSessionRecordId @user (sessionKey @user) diff --git a/IHP/ModelSupport.hs b/IHP/ModelSupport.hs index 7fa544dab..bc65d9c15 100644 --- a/IHP/ModelSupport.hs +++ b/IHP/ModelSupport.hs @@ -434,27 +434,57 @@ withTransactionConnection block = do let ?modelContext = modelContext in block {-# INLINABLE withTransactionConnection #-} --- | Returns the table name of a given model. --- --- __Example:__ --- --- >>> tableName @User --- "users" --- -tableName :: forall model. (KnownSymbol (GetTableName model)) => Text -tableName = symbolToText @(GetTableName model) -{-# INLINE tableName #-} +-- | Access meta data for a database table +class + ( KnownSymbol (GetTableName record) + ) => Table record where + -- | Returns the table name of a given model. + -- + -- __Example:__ + -- + -- >>> tableName @User + -- "users" + -- --- | Returns the table name of a given model as a bytestring. --- --- __Example:__ --- --- >>> tableNameByteString @User --- "users" --- -tableNameByteString :: forall model. (KnownSymbol (GetTableName model)) => ByteString -tableNameByteString = symbolToByteString @(GetTableName model) -{-# INLINE tableNameByteString #-} + tableName :: Text + default tableName :: forall model. (KnownSymbol (GetTableName model)) => Text + tableName = symbolToText @(GetTableName model) + {-# INLINE tableName #-} + + -- | Returns the table name of a given model as a bytestring. + -- + -- __Example:__ + -- + -- >>> tableNameByteString @User + -- "users" + -- + tableNameByteString :: ByteString + default tableNameByteString :: forall model. (KnownSymbol (GetTableName model)) => ByteString + tableNameByteString = symbolToByteString @(GetTableName model) + {-# INLINE tableNameByteString #-} + + -- | Returns the list of column names for a given model + -- + -- __Example:__ + -- + -- >>> columnNames @User + -- ["id", "email", "created_at"] + -- + columnNames :: [Text] + + -- | Returns WHERE conditions to match an entity by it's primary key + -- + -- For tables with a simple primary key this returns a tuple with the id: + -- + -- >>> primaryKeyCondition project + -- [("id", "d619f3cf-f355-4614-8a4c-e9ea4f301e39")] + -- + -- If the table has a composite primary key, this returns multiple elements: + -- + -- >>> primaryKeyCondition postTag + -- [("post_id", "0ace9270-568f-4188-b237-3789aa520588"), ("tag_id", "0b58fdf5-4bbb-4e57-a5b7-aa1c57148e1c")] + -- + primaryKeyCondition :: record -> [(Text, PG.Action)] logQuery :: (?modelContext :: ModelContext, Show query, Show parameters) => query -> parameters -> NominalDiffTime -> IO () logQuery query parameters time = do @@ -472,7 +502,7 @@ logQuery query parameters time = do -- DELETE FROM projects WHERE id = '..' -- -- Use 'deleteRecords' if you want to delete multiple records. -deleteRecord :: forall model id. (?modelContext :: ModelContext, KnownSymbol (GetTableName model), PrimaryKeyCondition model) => model -> IO () +deleteRecord :: forall model id. (?modelContext :: ModelContext, Table model) => model -> IO () deleteRecord model = do let condition = primaryKeyCondition model let whereConditions = condition |> map (\(field, _) -> field <> " = ?") |> intercalate " AND " @@ -488,7 +518,7 @@ deleteRecord model = do -- >>> delete projectId -- DELETE FROM projects WHERE id = '..' -- -deleteRecordById :: forall model id. (?modelContext :: ModelContext, Show id, KnownSymbol (GetTableName model), HasField "id" model id, ToField id) => id -> IO () +deleteRecordById :: forall model id. (?modelContext :: ModelContext, Show id, Table model, HasField "id" model id, ToField id) => id -> IO () deleteRecordById id = do let theQuery = "DELETE FROM " <> tableName @model <> " WHERE id = ?" let theParameters = (PG.Only id) @@ -501,7 +531,7 @@ deleteRecordById id = do -- >>> let projects :: [Project] = ... -- >>> deleteRecords projects -- DELETE FROM projects WHERE id IN (..) -deleteRecords :: forall record id. (?modelContext :: ModelContext, Show id, KnownSymbol (GetTableName record), HasField "id" record id, record ~ GetModelById id, ToField id) => [record] -> IO () +deleteRecords :: forall record id. (?modelContext :: ModelContext, Show id, Table record, HasField "id" record id, record ~ GetModelById id, ToField id) => [record] -> IO () deleteRecords records = do let theQuery = "DELETE FROM " <> tableName @record <> " WHERE id IN ?" let theParameters = PG.Only (PG.In (ids records)) @@ -513,7 +543,7 @@ deleteRecords records = do -- -- >>> deleteAll @Project -- DELETE FROM projects -deleteAll :: forall record. (?modelContext :: ModelContext, KnownSymbol (GetTableName record)) => IO () +deleteAll :: forall record. (?modelContext :: ModelContext, Table record) => IO () deleteAll = do let theQuery = "DELETE FROM " <> tableName @record sqlExec (PG.Query . cs $! theQuery) () @@ -787,18 +817,3 @@ withTableReadTracker trackedSection = do let ?modelContext = oldModelContext { trackTableReadCallback } let ?touchedTables = touchedTablesVar trackedSection - -class PrimaryKeyCondition record where - -- | Returns WHERE conditions to match an entity by it's primary key - -- - -- For tables with a simple primary key this returns a tuple with the id: - -- - -- >>> primaryKeyCondition project - -- [("id", "d619f3cf-f355-4614-8a4c-e9ea4f301e39")] - -- - -- If the table has a composite primary key, this returns multiple elements: - -- - -- >>> primaryKeyCondition postTag - -- [("post_id", "0ace9270-568f-4188-b237-3789aa520588"), ("tag_id", "0b58fdf5-4bbb-4e57-a5b7-aa1c57148e1c")] - -- - primaryKeyCondition :: record -> [(Text, PG.Action)] diff --git a/IHP/SchemaCompiler.hs b/IHP/SchemaCompiler.hs index 1d59989fb..ad1daeb89 100644 --- a/IHP/SchemaCompiler.hs +++ b/IHP/SchemaCompiler.hs @@ -159,14 +159,13 @@ compileStatement CompilerOptions { compileGetAndSetFieldInstances } (StatementCr <> compileGetModelName table <> compilePrimaryKeyInstance table <> section - <> compilePrimaryKeyConditionInstance table - <> section <> compileInclude table <> compileCreate table <> section <> compileUpdate table <> section <> compileBuild table + <> compileTableInstance table <> (if needsHasFieldId table then compileHasFieldId table else "") @@ -613,15 +612,18 @@ instance QueryBuilder.FilterPrimaryKey "#{name}" where primaryKeyFilter :: Column -> Text primaryKeyFilter Column {name} = "QueryBuilder.filterWhere (#" <> columnNameToFieldName name <> ", " <> columnNameToFieldName name <> ")" -compilePrimaryKeyConditionInstance :: (?schema :: Schema) => CreateTable -> Text -compilePrimaryKeyConditionInstance table@(CreateTable { name, columns, constraints }) = cs [i| +compileTableInstance :: (?schema :: Schema) => CreateTable -> Text +compileTableInstance table@(CreateTable { name, columns, constraints }) = cs [i| instance #{instanceHead} where + tableName = \"#{name}\" + tableNameByteString = \"#{name}\" + columnNames = #{columnNames} primaryKeyCondition #{pattern} = #{condition} {-# INLINABLE primaryKeyCondition #-} |] where instanceHead :: Text - instanceHead = instanceConstraints <> " => PrimaryKeyCondition (" <> compileTypePattern table <> ")" + instanceHead = instanceConstraints <> " => Table (" <> compileTypePattern table <> ")" where instanceConstraints = table @@ -651,6 +653,10 @@ instance #{instanceHead} where primaryKeyToCondition :: Column -> Text primaryKeyToCondition column = "(\"" <> get #name column <> "\", toField " <> columnNameToFieldName (get #name column) <> ")" + columnNames = columns + |> map (get #name) + |> tshow + compileGetModelName :: (?schema :: Schema) => CreateTable -> Text compileGetModelName table@(CreateTable { name }) = "type instance GetModelName (" <> tableNameToModelName name <> "' " <> unwords (map (const "_") (dataTypeArguments table)) <> ") = " <> tshow (tableNameToModelName name) <> "\n" diff --git a/IHP/ValidationSupport/ValidateCanView.hs b/IHP/ValidationSupport/ValidateCanView.hs index f66b35383..1605cdfd7 100644 --- a/IHP/ValidationSupport/ValidateCanView.hs +++ b/IHP/ValidationSupport/ValidateCanView.hs @@ -4,6 +4,7 @@ import IHP.Prelude import qualified Database.PostgreSQL.Simple as PG import IHP.AuthSupport.Authorization import IHP.Fetch (Fetchable, fetchOneOrNothing) +import IHP.ModelSupport (Table) import IHP.ValidationSupport.Types validateCanView :: forall field user model validationState fieldValue validationStateValue fetchedModel. ( @@ -18,6 +19,7 @@ validateCanView :: forall field user model validationState fieldValue validation , ValidateCanView' fieldValue fetchedModel , HasField "meta" user MetaBag , SetField "meta" user MetaBag + , Table fetchedModel ) => Proxy field -> user -> IO user validateCanView field user = do let id = getField @field ?model @@ -36,16 +38,16 @@ validateCanView field user = do -- -- Therefore we have to handle this special of `Maybe TeamId` with the following type class. class ValidateCanView' id model where - doValidateCanView :: (?modelContext :: ModelContext, CanView user model, Fetchable id model, KnownSymbol (GetTableName model), PG.FromRow model) => Proxy model -> user -> id -> IO ValidatorResult + doValidateCanView :: (?modelContext :: ModelContext, CanView user model, Fetchable id model, KnownSymbol (GetTableName model), PG.FromRow model, Table model) => Proxy model -> user -> id -> IO ValidatorResult -- Maybe someId -instance {-# OVERLAPS #-} (ValidateCanView' id' model, Fetchable id' model) => ValidateCanView' (Maybe id') model where +instance {-# OVERLAPS #-} (ValidateCanView' id' model, Fetchable id' model, Table model) => ValidateCanView' (Maybe id') model where -- doValidateCanView :: (?modelContext :: ModelContext, CanView user model, Fetchable id model, KnownSymbol (GetTableName model), PG.FromRow model) => Proxy model -> user -> (Maybe id) -> IO ValidatorResult doValidateCanView model user id = maybe (pure Success) (doValidateCanView model user) id -- Catch all instance {-# OVERLAPPABLE #-} ValidateCanView' any model where - doValidateCanView :: (?modelContext :: ModelContext, CanView user model, Fetchable id model, KnownSymbol (GetTableName model), PG.FromRow model) => Proxy model -> user -> id -> IO ValidatorResult + doValidateCanView :: (?modelContext :: ModelContext, CanView user model, Fetchable id model, KnownSymbol (GetTableName model), PG.FromRow model, Table model) => Proxy model -> user -> id -> IO ValidatorResult doValidateCanView model user id = do fetchedModel <- liftIO (fetchOneOrNothing id) canView' <- maybe (pure False) (\fetchedModel -> canView fetchedModel user) fetchedModel diff --git a/IHP/ValidationSupport/ValidateIsUnique.hs b/IHP/ValidationSupport/ValidateIsUnique.hs index 5cf832c93..cee6fb252 100644 --- a/IHP/ValidationSupport/ValidateIsUnique.hs +++ b/IHP/ValidationSupport/ValidateIsUnique.hs @@ -45,6 +45,7 @@ validateIsUnique :: forall field model savedModel validationState fieldValue val , savedModelId ~ modelId , Eq modelId , GetModelByTableName (GetTableName savedModel) ~ savedModel + , Table savedModel ) => Proxy field -> model -> IO model validateIsUnique fieldProxy model = validateIsUniqueCaseAware fieldProxy model True {-# INLINE validateIsUnique #-} @@ -85,6 +86,7 @@ validateIsUniqueCaseInsensitive :: forall field model savedModel validationState , savedModelId ~ modelId , Eq modelId , GetModelByTableName (GetTableName savedModel) ~ savedModel + , Table savedModel ) => Proxy field -> model -> IO model validateIsUniqueCaseInsensitive fieldProxy model = validateIsUniqueCaseAware fieldProxy model False {-# INLINE validateIsUniqueCaseInsensitive #-} @@ -107,6 +109,7 @@ validateIsUniqueCaseAware :: forall field model savedModel validationState field , savedModelId ~ modelId , Eq modelId , GetModelByTableName (GetTableName savedModel) ~ savedModel + , Table savedModel ) => Proxy field -> model -> Bool -> IO model validateIsUniqueCaseAware fieldProxy model caseSensitive = do let value = getField @field model