Convert db.clj public functions to mu/defn with schemas (#82139)
This commit is contained in:
committed by
GitHub
parent
22b2577799
commit
c1805286bb
@@ -146,6 +146,7 @@
|
||||
collections
|
||||
config
|
||||
events
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
util}
|
||||
@@ -158,7 +159,10 @@
|
||||
|
||||
agent-api
|
||||
{:team "Metabot"
|
||||
:api #{metabase.agent-api.api metabase.agent-api.init metabase.agent-api.usage}
|
||||
:api #{metabase.agent-api.api
|
||||
metabase.agent-api.init
|
||||
metabase.agent-api.schema
|
||||
metabase.agent-api.usage}
|
||||
:uses #{ai-tracing
|
||||
api
|
||||
auth-identity
|
||||
@@ -222,6 +226,7 @@
|
||||
driver
|
||||
eid-translation
|
||||
internal-stats
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
permissions
|
||||
@@ -291,6 +296,7 @@
|
||||
:uses #{api-scope
|
||||
config
|
||||
events
|
||||
lib
|
||||
models
|
||||
settings
|
||||
util}
|
||||
@@ -325,6 +331,7 @@
|
||||
:uses #{api
|
||||
app-db
|
||||
events
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
users
|
||||
@@ -363,17 +370,17 @@
|
||||
{:team "UX West"
|
||||
:api #{metabase.appearance.core
|
||||
metabase.appearance.init}
|
||||
:uses #{settings util}
|
||||
:uses #{lib settings util}
|
||||
:model-imports #{:model/Dashboard}}
|
||||
|
||||
audit-app
|
||||
{:team "UX West"
|
||||
:api #{metabase.audit-app.core
|
||||
metabase.audit-app.init}
|
||||
:api #{metabase.audit-app.core metabase.audit-app.init}
|
||||
:friends #{enterprise/memoize-monitor}
|
||||
:uses #{api
|
||||
app-db
|
||||
events
|
||||
lib
|
||||
models
|
||||
premium-features
|
||||
settings
|
||||
@@ -407,14 +414,19 @@
|
||||
|
||||
auth-identity
|
||||
{:team "UX West"
|
||||
:api #{metabase.auth-identity.core metabase.auth-identity.db metabase.auth-identity.init}
|
||||
:api #{metabase.auth-identity.core
|
||||
metabase.auth-identity.db
|
||||
metabase.auth-identity.init
|
||||
metabase.auth-identity.schema}
|
||||
:uses #{channel
|
||||
events
|
||||
lib
|
||||
login-history
|
||||
models
|
||||
notification
|
||||
premium-features
|
||||
request
|
||||
users
|
||||
util}
|
||||
:model-exports #{:model/AuthIdentity}
|
||||
:model-imports #{:model/Session :model/User}}
|
||||
@@ -438,6 +450,7 @@
|
||||
:uses #{api
|
||||
app-db
|
||||
collections
|
||||
lib
|
||||
permissions
|
||||
queries
|
||||
util}
|
||||
@@ -464,9 +477,7 @@
|
||||
|
||||
cache
|
||||
{:team "UX West"
|
||||
:api #{metabase.cache.api
|
||||
metabase.cache.core
|
||||
metabase.cache.init}
|
||||
:api #{metabase.cache.api metabase.cache.core metabase.cache.init}
|
||||
:uses #{api
|
||||
app-db
|
||||
config
|
||||
@@ -601,6 +612,7 @@
|
||||
audit-app
|
||||
config
|
||||
events
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
@@ -633,6 +645,7 @@
|
||||
documents
|
||||
eid-translation
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
notification
|
||||
@@ -665,6 +678,7 @@
|
||||
api
|
||||
channel
|
||||
events
|
||||
lib
|
||||
models
|
||||
request
|
||||
users
|
||||
@@ -687,16 +701,16 @@
|
||||
|
||||
content-translation
|
||||
{:team "UX West"
|
||||
:api #{metabase.content-translation.models}
|
||||
:api #{metabase.content-translation.models metabase.content-translation.schema}
|
||||
:uses #{premium-features
|
||||
util}
|
||||
:model-exports #{:model/ContentTranslation}}
|
||||
|
||||
content-verification
|
||||
{:team "UX West"
|
||||
:api #{metabase.content-verification.core
|
||||
metabase.content-verification.init}
|
||||
:api #{metabase.content-verification.core metabase.content-verification.init}
|
||||
:uses #{app-db
|
||||
lib
|
||||
models
|
||||
util}
|
||||
:model-exports #{:model/ModerationReview}
|
||||
@@ -809,6 +823,7 @@
|
||||
database-routing
|
||||
driver
|
||||
events
|
||||
lib
|
||||
request
|
||||
sync
|
||||
util
|
||||
@@ -837,6 +852,7 @@
|
||||
batch-processing
|
||||
collections
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
parameters
|
||||
@@ -961,6 +977,7 @@
|
||||
{:team "Embedding"
|
||||
:api #{metabase.embedding.init
|
||||
metabase.embedding.jwt
|
||||
metabase.embedding.schema
|
||||
metabase.embedding.settings
|
||||
metabase.embedding.util
|
||||
metabase.embedding.validation}
|
||||
@@ -1004,7 +1021,7 @@
|
||||
{:team "Metabot"
|
||||
:api #{metabase.entity-retrieval.core
|
||||
metabase.entity-retrieval.mirror}
|
||||
:uses #{premium-features}
|
||||
:uses #{premium-features util}
|
||||
:model-imports #{:model/OsiAiContext}}
|
||||
|
||||
events
|
||||
@@ -1028,9 +1045,7 @@
|
||||
|
||||
explorations
|
||||
{:team "UX West"
|
||||
:api #{metabase.explorations.api
|
||||
metabase.explorations.core
|
||||
metabase.explorations.init}
|
||||
:api #{metabase.explorations.api metabase.explorations.core metabase.explorations.init}
|
||||
:uses #{analytics
|
||||
analytics-interface
|
||||
api
|
||||
@@ -1103,6 +1118,7 @@
|
||||
:api #{metabase.glossary.api}
|
||||
:uses #{api
|
||||
events
|
||||
lib
|
||||
models
|
||||
util}
|
||||
:model-exports #{:model/Glossary}
|
||||
@@ -1127,8 +1143,7 @@
|
||||
|
||||
indexed-entities
|
||||
{:team "UX West"
|
||||
:api #{metabase.indexed-entities.api
|
||||
metabase.indexed-entities.init}
|
||||
:api #{metabase.indexed-entities.api metabase.indexed-entities.init}
|
||||
:uses #{analytics
|
||||
api
|
||||
driver
|
||||
@@ -1150,6 +1165,7 @@
|
||||
metabase.indexes.reconcile
|
||||
metabase.indexes.schema}
|
||||
:uses #{driver
|
||||
lib
|
||||
models
|
||||
util}
|
||||
:model-exports #{:model/TableIndex}}
|
||||
@@ -1159,6 +1175,7 @@
|
||||
:api #{metabase.indexes-rest.api}
|
||||
:uses #{api
|
||||
indexes
|
||||
lib
|
||||
transforms-base
|
||||
util}
|
||||
:model-imports #{:model/Database
|
||||
@@ -1178,8 +1195,7 @@
|
||||
internal-stats
|
||||
{:team "UX West"
|
||||
:api #{metabase.internal-stats.core}
|
||||
:uses #{app-db
|
||||
models}
|
||||
:uses #{app-db models util}
|
||||
:model-imports #{:model/AiUsageLog
|
||||
:model/Card
|
||||
:model/Dashboard
|
||||
@@ -1316,13 +1332,12 @@
|
||||
|
||||
login-history
|
||||
{:team "UX West"
|
||||
:api #{metabase.login-history.api
|
||||
metabase.login-history.core
|
||||
metabase.login-history.init}
|
||||
:api #{metabase.login-history.api metabase.login-history.core metabase.login-history.init}
|
||||
:uses #{analytics
|
||||
api
|
||||
app-db
|
||||
channel
|
||||
lib
|
||||
request
|
||||
settings
|
||||
util}
|
||||
@@ -1334,6 +1349,7 @@
|
||||
metabase.mcp.callback-api
|
||||
metabase.mcp.core
|
||||
metabase.mcp.init
|
||||
metabase.mcp.schema
|
||||
metabase.mcp.usage}
|
||||
:model-exports #{:model/McpSessionLog
|
||||
:model/McpToolCallLog}
|
||||
@@ -1342,6 +1358,7 @@
|
||||
api
|
||||
app-db
|
||||
config
|
||||
lib
|
||||
llm
|
||||
metabot
|
||||
oauth-server
|
||||
@@ -1355,7 +1372,7 @@
|
||||
|
||||
measures
|
||||
{:team "Graphy"
|
||||
:api #{metabase.measures.api}
|
||||
:api #{metabase.measures.api metabase.measures.schema}
|
||||
:uses #{api
|
||||
events
|
||||
lib
|
||||
@@ -1376,6 +1393,7 @@
|
||||
metabase.metabot.core
|
||||
metabase.metabot.init
|
||||
metabase.metabot.metadata-perms
|
||||
metabase.metabot.schema
|
||||
metabase.metabot.self
|
||||
metabase.metabot.settings
|
||||
metabase.metabot.tools.entity-details}
|
||||
@@ -1473,9 +1491,7 @@
|
||||
|
||||
model-persistence
|
||||
{:team "Graphy"
|
||||
:api #{metabase.model-persistence.api
|
||||
metabase.model-persistence.core
|
||||
metabase.model-persistence.init}
|
||||
:api #{metabase.model-persistence.api metabase.model-persistence.core metabase.model-persistence.init}
|
||||
:uses #{api
|
||||
app-db
|
||||
driver
|
||||
@@ -1492,7 +1508,8 @@
|
||||
task
|
||||
task-history
|
||||
tracing
|
||||
util}
|
||||
util
|
||||
warehouses}
|
||||
:model-exports #{:model/PersistedInfo}
|
||||
:model-imports #{:model/Card :model/Database}}
|
||||
|
||||
@@ -1542,8 +1559,7 @@
|
||||
|
||||
native-query-snippets
|
||||
{:team "Graphy"
|
||||
:api #{metabase.native-query-snippets.api
|
||||
metabase.native-query-snippets.core}
|
||||
:api #{metabase.native-query-snippets.api metabase.native-query-snippets.core}
|
||||
:uses #{api
|
||||
collections
|
||||
events
|
||||
@@ -1612,6 +1628,7 @@
|
||||
api-scope
|
||||
appearance
|
||||
events
|
||||
lib
|
||||
mcp
|
||||
models
|
||||
request
|
||||
@@ -1682,6 +1699,7 @@
|
||||
remote-sync
|
||||
settings
|
||||
tracing
|
||||
users
|
||||
util
|
||||
enterprise/advanced-permissions}
|
||||
:model-exports #{:model/ApplicationPermissionsRevision
|
||||
@@ -1776,9 +1794,7 @@
|
||||
:api #{metabase.public-sharing.core
|
||||
metabase.public-sharing.init
|
||||
metabase.public-sharing.validation}
|
||||
:uses #{api
|
||||
settings
|
||||
util}
|
||||
:uses #{api settings util}
|
||||
:model-imports #{:model/Action
|
||||
:model/Card
|
||||
:model/Dashboard
|
||||
@@ -1811,9 +1827,7 @@
|
||||
|
||||
pulse
|
||||
{:team "Gadget"
|
||||
:api #{metabase.pulse.api
|
||||
metabase.pulse.core
|
||||
metabase.pulse.init}
|
||||
:api #{metabase.pulse.api metabase.pulse.core metabase.pulse.init}
|
||||
:uses #{api
|
||||
app-db
|
||||
channel
|
||||
@@ -2040,8 +2054,10 @@
|
||||
metabase.remote-sync.init}
|
||||
:uses #{api
|
||||
events
|
||||
lib
|
||||
models
|
||||
premium-features}
|
||||
premium-features
|
||||
util}
|
||||
:model-imports #{:model/Collection}}
|
||||
|
||||
request
|
||||
@@ -2055,7 +2071,8 @@
|
||||
api
|
||||
config
|
||||
embedding
|
||||
permissions ; FIXME
|
||||
lib
|
||||
permissions
|
||||
premium-features
|
||||
session
|
||||
settings
|
||||
@@ -2107,10 +2124,12 @@
|
||||
:api #{metabase.sample-data.core
|
||||
metabase.sample-data.init}
|
||||
:uses #{driver
|
||||
lib
|
||||
plugins
|
||||
settings
|
||||
sync
|
||||
util}
|
||||
util
|
||||
warehouses}
|
||||
:model-imports #{:model/DataPermissions :model/Database :model/Table}}
|
||||
|
||||
search
|
||||
@@ -2158,10 +2177,21 @@
|
||||
:model/Table
|
||||
:model/User}}
|
||||
|
||||
|
||||
secrets
|
||||
{:team "Graphy"
|
||||
:api #{metabase.secrets.core}
|
||||
:uses #{api driver models premium-features util}}
|
||||
:uses #{api
|
||||
driver
|
||||
lib
|
||||
models
|
||||
premium-features
|
||||
util}}
|
||||
|
||||
security-center
|
||||
{:team "Gadget"
|
||||
:api #{metabase.security-center.schema}
|
||||
:uses #{util}}
|
||||
|
||||
segments
|
||||
{:team "Graphy"
|
||||
@@ -2199,6 +2229,7 @@
|
||||
driver
|
||||
embedding
|
||||
initialization-status
|
||||
lib
|
||||
lib-be
|
||||
mcp
|
||||
oauth-server
|
||||
@@ -2228,6 +2259,7 @@
|
||||
channel
|
||||
config
|
||||
events
|
||||
lib
|
||||
premium-features
|
||||
request
|
||||
settings
|
||||
@@ -2246,6 +2278,7 @@
|
||||
app-db
|
||||
config
|
||||
events
|
||||
lib
|
||||
models
|
||||
premium-features
|
||||
util
|
||||
@@ -2267,6 +2300,7 @@
|
||||
metabase.setup.init}
|
||||
:uses #{app-db
|
||||
config
|
||||
lib
|
||||
settings
|
||||
util}
|
||||
:model-imports #{:model/User}}
|
||||
@@ -2279,6 +2313,7 @@
|
||||
appearance
|
||||
auth-identity
|
||||
events
|
||||
lib
|
||||
request
|
||||
settings
|
||||
setup
|
||||
@@ -2353,6 +2388,7 @@
|
||||
:uses #{api
|
||||
auth-identity
|
||||
config
|
||||
lib
|
||||
permissions
|
||||
premium-features
|
||||
request
|
||||
@@ -2433,11 +2469,10 @@
|
||||
|
||||
task-history
|
||||
{:team "UX West"
|
||||
:api #{metabase.task-history.api
|
||||
metabase.task-history.core
|
||||
metabase.task-history.init}
|
||||
:api #{metabase.task-history.api metabase.task-history.core metabase.task-history.init}
|
||||
:uses #{api
|
||||
config
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
@@ -2469,9 +2504,11 @@
|
||||
lib-be
|
||||
llm
|
||||
mcp
|
||||
metabot
|
||||
permissions
|
||||
premium-features
|
||||
search
|
||||
security-center
|
||||
session
|
||||
util}
|
||||
:model-imports #{:model/AiUsageLog
|
||||
@@ -2502,12 +2539,12 @@
|
||||
|
||||
timeline
|
||||
{:team "UX West"
|
||||
:api #{metabase.timeline.api
|
||||
metabase.timeline.core}
|
||||
:api #{metabase.timeline.api metabase.timeline.core}
|
||||
:uses #{analytics
|
||||
api
|
||||
collections
|
||||
events
|
||||
lib
|
||||
models
|
||||
util}
|
||||
:model-exports #{:model/Timeline}
|
||||
@@ -2610,6 +2647,7 @@
|
||||
metabase.transforms-rest.api.transform-job
|
||||
metabase.transforms-rest.api.transform-tag}
|
||||
:uses #{api
|
||||
lib
|
||||
models
|
||||
request
|
||||
transforms
|
||||
@@ -2725,6 +2763,7 @@
|
||||
channel
|
||||
config
|
||||
events
|
||||
lib
|
||||
models
|
||||
notification
|
||||
permissions
|
||||
@@ -2751,6 +2790,7 @@
|
||||
collections
|
||||
config
|
||||
events
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
@@ -2786,14 +2826,14 @@
|
||||
|
||||
view-log
|
||||
{:team "UX West"
|
||||
:api #{metabase.view-log.core
|
||||
metabase.view-log.init}
|
||||
:api #{metabase.view-log.core metabase.view-log.init}
|
||||
:uses #{analytics
|
||||
api
|
||||
app-db
|
||||
audit-app
|
||||
batch-processing
|
||||
events
|
||||
lib
|
||||
models
|
||||
premium-features
|
||||
query-permissions
|
||||
@@ -2814,6 +2854,7 @@
|
||||
metabase.warehouse-schema.models.field-user-settings
|
||||
metabase.warehouse-schema.models.field-values
|
||||
metabase.warehouse-schema.models.table
|
||||
metabase.warehouse-schema.schema
|
||||
metabase.warehouse-schema.table}
|
||||
:uses #{analyze
|
||||
api
|
||||
@@ -2879,7 +2920,8 @@
|
||||
{:team "UX West"
|
||||
:api #{metabase.warehouses.core
|
||||
metabase.warehouses.init
|
||||
metabase.warehouses.models.database}
|
||||
metabase.warehouses.models.database
|
||||
metabase.warehouses.schema}
|
||||
:uses #{analytics-interface
|
||||
api
|
||||
app-db
|
||||
@@ -3008,7 +3050,8 @@
|
||||
setup
|
||||
sync
|
||||
users
|
||||
util}
|
||||
util
|
||||
warehouses}
|
||||
:model-imports #{:model/ApiKey :model/Database :model/User}}
|
||||
|
||||
enterprise/advanced-permissions
|
||||
@@ -3051,6 +3094,7 @@
|
||||
:api #{}
|
||||
:uses #{driver
|
||||
premium-features
|
||||
util
|
||||
enterprise/advanced-config
|
||||
enterprise/mfa
|
||||
enterprise/scim
|
||||
@@ -3085,6 +3129,7 @@
|
||||
permissions
|
||||
plugins
|
||||
premium-features
|
||||
queries
|
||||
query-permissions
|
||||
query-processor
|
||||
settings
|
||||
@@ -3093,6 +3138,8 @@
|
||||
sync
|
||||
users
|
||||
util
|
||||
warehouse-schema
|
||||
warehouses
|
||||
enterprise/serialization}
|
||||
:model-imports #{:model/AuditLog
|
||||
:model/Card
|
||||
@@ -3125,6 +3172,7 @@
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.billing.api.routes}
|
||||
:uses #{api
|
||||
lib
|
||||
premium-features
|
||||
store-api
|
||||
util}
|
||||
@@ -3134,6 +3182,7 @@
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.cache.init}
|
||||
:uses #{cache
|
||||
lib
|
||||
premium-features
|
||||
query-processor
|
||||
task
|
||||
@@ -3186,6 +3235,7 @@
|
||||
:api #{metabase-enterprise.content-verification.api.routes}
|
||||
:uses #{api
|
||||
content-verification
|
||||
lib
|
||||
util
|
||||
enterprise/api}
|
||||
:model-imports #{:model/Card :model/Dashboard}}
|
||||
@@ -3240,6 +3290,7 @@
|
||||
config
|
||||
driver
|
||||
embeddings
|
||||
lib
|
||||
metabot
|
||||
models
|
||||
premium-features
|
||||
@@ -3263,6 +3314,7 @@
|
||||
:uses #{api
|
||||
collections
|
||||
events
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
@@ -3280,6 +3332,7 @@
|
||||
metabase-enterprise.database-replication.init}
|
||||
:uses #{api
|
||||
driver
|
||||
lib
|
||||
premium-features
|
||||
settings
|
||||
util
|
||||
@@ -3367,8 +3420,10 @@
|
||||
appearance
|
||||
audit-app
|
||||
embedding
|
||||
lib
|
||||
permissions
|
||||
premium-features
|
||||
util
|
||||
enterprise/sso}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
@@ -3396,6 +3451,7 @@
|
||||
collections
|
||||
entity-retrieval
|
||||
health-inspector
|
||||
lib
|
||||
premium-features
|
||||
search
|
||||
task
|
||||
@@ -3411,6 +3467,7 @@
|
||||
{:team "Gadget"
|
||||
:api #{metabase-enterprise.erd.api}
|
||||
:uses #{api
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
util
|
||||
@@ -3426,9 +3483,11 @@
|
||||
:uses #{analytics
|
||||
analytics-interface
|
||||
api
|
||||
lib
|
||||
premium-features
|
||||
settings
|
||||
util
|
||||
warehouses
|
||||
enterprise/harbormaster}
|
||||
:model-imports #{:model/Database :model/Setting}}
|
||||
|
||||
@@ -3466,13 +3525,16 @@
|
||||
:api #{metabase-enterprise.library.api}
|
||||
:uses #{api
|
||||
collections
|
||||
premium-features}
|
||||
lib
|
||||
premium-features
|
||||
util}
|
||||
:model-imports #{:model/Card :model/Collection :model/Table}}
|
||||
|
||||
enterprise/mcp
|
||||
{:team "Metabot"
|
||||
:api #{metabase-enterprise.mcp.init}
|
||||
:uses #{analytics
|
||||
lib
|
||||
mcp
|
||||
metabot
|
||||
premium-features
|
||||
@@ -3504,6 +3566,7 @@
|
||||
mcp
|
||||
metabot}
|
||||
:uses #{api
|
||||
lib
|
||||
lib-be
|
||||
llm
|
||||
metabot
|
||||
@@ -3527,6 +3590,7 @@
|
||||
:api #{metabase-enterprise.metabot-analytics.api}
|
||||
:uses #{api
|
||||
driver
|
||||
lib
|
||||
metabot
|
||||
permissions
|
||||
query-processor
|
||||
@@ -3547,6 +3611,7 @@
|
||||
channel
|
||||
config
|
||||
events
|
||||
lib
|
||||
premium-features
|
||||
request
|
||||
settings
|
||||
@@ -3581,6 +3646,7 @@
|
||||
app-db
|
||||
collections
|
||||
events
|
||||
lib
|
||||
models
|
||||
premium-features
|
||||
search
|
||||
@@ -3615,12 +3681,15 @@
|
||||
:uses #{analytics
|
||||
api
|
||||
app-db
|
||||
dashboards
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
measures
|
||||
model-persistence
|
||||
models
|
||||
queries
|
||||
segments
|
||||
source-swap
|
||||
task
|
||||
transforms
|
||||
@@ -3640,8 +3709,7 @@
|
||||
|
||||
enterprise/sandbox
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.sandbox.api.routes
|
||||
metabase-enterprise.sandbox.api.util}
|
||||
:api #{metabase-enterprise.sandbox.api.routes metabase-enterprise.sandbox.api.util}
|
||||
:uses #{api
|
||||
app-db
|
||||
audit-app
|
||||
@@ -3653,6 +3721,7 @@
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
queries
|
||||
query-processor
|
||||
request
|
||||
tenants
|
||||
@@ -3678,6 +3747,7 @@
|
||||
:uses #{analytics-interface
|
||||
api
|
||||
api-keys
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
server
|
||||
@@ -3698,8 +3768,7 @@
|
||||
|
||||
enterprise/security-center
|
||||
{:team "Gadget"
|
||||
:api #{metabase-enterprise.security-center.api
|
||||
metabase-enterprise.security-center.init}
|
||||
:api #{metabase-enterprise.security-center.api metabase-enterprise.security-center.init}
|
||||
:uses #{analytics
|
||||
analytics-interface
|
||||
api
|
||||
@@ -3708,10 +3777,12 @@
|
||||
config
|
||||
driver
|
||||
events
|
||||
lib
|
||||
models
|
||||
notification
|
||||
permissions
|
||||
premium-features
|
||||
security-center
|
||||
settings
|
||||
system
|
||||
task
|
||||
@@ -3739,6 +3810,7 @@
|
||||
embeddings
|
||||
events
|
||||
health-inspector
|
||||
lib
|
||||
lib-be
|
||||
llm
|
||||
metabot
|
||||
@@ -3783,7 +3855,8 @@
|
||||
enterprise/snippet-collections
|
||||
{:team "Graphy"
|
||||
:api #{}
|
||||
:uses #{models
|
||||
:uses #{lib
|
||||
models
|
||||
native-query-snippets
|
||||
permissions
|
||||
premium-features
|
||||
@@ -3819,6 +3892,7 @@
|
||||
:uses #{analytics
|
||||
api
|
||||
collections
|
||||
lib
|
||||
premium-features
|
||||
queries
|
||||
request
|
||||
@@ -3835,11 +3909,13 @@
|
||||
app-db
|
||||
auth-identity
|
||||
events
|
||||
lib
|
||||
models
|
||||
request
|
||||
settings
|
||||
system
|
||||
task
|
||||
users
|
||||
util}
|
||||
:model-imports #{:model/AuthIdentity :model/Session :model/User}}
|
||||
|
||||
@@ -3851,6 +3927,7 @@
|
||||
auth-identity
|
||||
collections
|
||||
events
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
@@ -3863,7 +3940,8 @@
|
||||
enterprise/transforms
|
||||
{:team "Graphy"
|
||||
:api #{metabase-enterprise.transforms.api}
|
||||
:uses #{premium-features
|
||||
:uses #{lib
|
||||
premium-features
|
||||
util
|
||||
enterprise/transforms-inspector}
|
||||
:model-imports #{:model/Transform}}
|
||||
@@ -3920,6 +3998,7 @@
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.upload-management.api}
|
||||
:uses #{api
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
|
||||
@@ -2,53 +2,59 @@
|
||||
"Application database queries for the action-v2 module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for model definitions, hydration methods, and transactions."
|
||||
(:require
|
||||
[metabase-enterprise.action-v2.schema :as action-v2.schema]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util :as u]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn table
|
||||
(mu/defn table
|
||||
"The Table with `table-id`, or nil."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one :model/Table table-id))
|
||||
|
||||
(defn active-table
|
||||
(mu/defn active-table
|
||||
"The active Table with `table-id`, or nil."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one :model/Table :id table-id :active true))
|
||||
|
||||
(defn database
|
||||
(mu/defn database
|
||||
"The Database with `database-id`, or nil."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select-one :model/Database :id database-id))
|
||||
|
||||
(defn fields
|
||||
(mu/defn fields
|
||||
"The Fields with `field-ids`."
|
||||
[field-ids]
|
||||
[field-ids :- [:sequential ::lib.schema.id/field]]
|
||||
(t2/select :model/Field :id [:in field-ids]))
|
||||
|
||||
(defn pk-fields-for-table
|
||||
(mu/defn pk-fields-for-table
|
||||
"The active primary key Fields of the Table with `table-id`."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select :model/Field :table_id table-id :semantic_type :type/PK :active true))
|
||||
|
||||
(defn fields-by-name
|
||||
(mu/defn fields-by-name
|
||||
"The Fields of the Table with `table-id` named one of `field-names`."
|
||||
[table-id field-names]
|
||||
[table-id :- ::lib.schema.id/table
|
||||
field-names :- [:sequential :string]]
|
||||
(t2/select :model/Field :table_id table-id :name [:in field-names]))
|
||||
|
||||
(defn active-fields-in-position-order
|
||||
(mu/defn active-fields-in-position-order
|
||||
"The active Fields of the Table with `table-id`, in position order."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select :model/Field :table_id table-id :active true {:order-by [[:position]]}))
|
||||
|
||||
(defn field-requirements-by-name
|
||||
(mu/defn field-requirements-by-name
|
||||
"A map of name to the name, required flag, and base type of the Fields of the Table with `table-id`."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-fn->fn :name identity [:model/Field :name :database_required :base_type] :table_id table-id))
|
||||
|
||||
(defn category-list-field-ids-by-name
|
||||
(mu/defn category-list-field-ids-by-name
|
||||
"The `:id` and `:lower_name` rows of the category list Fields of the Table with `table-id` whose name matches one
|
||||
of `names`, case-insensitively."
|
||||
[table-id names]
|
||||
[table-id :- ::lib.schema.id/table
|
||||
names :- [:sequential :string]]
|
||||
(t2/query {:select [:id [[:lower :name] :lower_name]]
|
||||
:from [(t2/table-name :model/Field)]
|
||||
:where [:and
|
||||
@@ -57,9 +63,9 @@
|
||||
[:in :has_field_values ["list" "auto-list"]]
|
||||
[:= :semantic_type "type/Category"]]}))
|
||||
|
||||
(defn field-values-of-fields
|
||||
(mu/defn field-values-of-fields
|
||||
"The value lists of the FieldValues of the Fields with `field-ids`."
|
||||
[field-ids]
|
||||
[field-ids :- [:sequential ::lib.schema.id/field]]
|
||||
(t2/select-fn-vec :values :model/FieldValues :field_id [:in field-ids]))
|
||||
|
||||
(defn- scope-and-user-expr
|
||||
@@ -68,9 +74,11 @@
|
||||
(when scope [:= :scope scope])
|
||||
(when user-id [:= :user_id user-id])])
|
||||
|
||||
(defn next-undo-batch
|
||||
(mu/defn next-undo-batch
|
||||
"The Undo rows of the newest not-undone (when `undo?`) or oldest undone batch of `user-id` in `scope`."
|
||||
[undo? user-id scope]
|
||||
[undo? :- :boolean
|
||||
user-id :- ::lib.schema.id/user
|
||||
scope :- :string]
|
||||
(t2/select :model/Undo
|
||||
:batch_num [:in
|
||||
^:allow-subquery
|
||||
@@ -83,10 +91,12 @@
|
||||
[:not :undone]
|
||||
:undone)]}]))
|
||||
|
||||
(defn batch-to-prune-from
|
||||
(mu/defn batch-to-prune-from
|
||||
"The `:batch_num` row of the newest Undo batch beyond the `batches-to-keep` most recent ones, narrowed by the
|
||||
optional `scope` and `user-id`, or nil."
|
||||
[batches-to-keep scope user-id]
|
||||
[batches-to-keep :- ms/PositiveInt
|
||||
scope :- [:maybe :string]
|
||||
user-id :- [:maybe ::lib.schema.id/user]]
|
||||
(t2/query {:select [:batch_num]
|
||||
:from [(t2/table-name :model/Undo)]
|
||||
:where (scope-and-user-expr scope user-id)
|
||||
@@ -95,56 +105,66 @@
|
||||
:limit 1
|
||||
:offset batches-to-keep}))
|
||||
|
||||
(defn batch-to-prune-from-for-rows
|
||||
(mu/defn batch-to-prune-from-for-rows
|
||||
"The `:batch_num` row of the Undo row beyond the `rows-to-keep` most recent ones, or nil."
|
||||
[rows-to-keep]
|
||||
[rows-to-keep :- ms/PositiveInt]
|
||||
(t2/query {:select [:batch_num]
|
||||
:from [(t2/table-name :model/Undo)]
|
||||
:order-by [[:id :desc]]
|
||||
:limit 1
|
||||
:offset rows-to-keep}))
|
||||
|
||||
(defn delete-undo-batches-up-to!
|
||||
(mu/defn delete-undo-batches-up-to!
|
||||
"Delete the Undo rows of batches up to `batch-num`, narrowed by the optional `scope` and `user-id`."
|
||||
[batch-num scope user-id]
|
||||
[batch-num :- ms/IntGreaterThanOrEqualToZero
|
||||
scope :- [:maybe :string]
|
||||
user-id :- [:maybe ::lib.schema.id/user]]
|
||||
(t2/delete! :model/Undo :batch_num [:<= batch-num] {:where (scope-and-user-expr scope user-id)}))
|
||||
|
||||
(defn delete-undone-batches-from!
|
||||
(mu/defn delete-undone-batches-from!
|
||||
"Delete the undone Undo rows in `scope` from batch `batch-num` onwards."
|
||||
[batch-num scope]
|
||||
[batch-num :- ms/PositiveInt
|
||||
scope :- :string]
|
||||
(t2/delete! :model/Undo :batch_num [:>= batch-num] :scope scope :undone true))
|
||||
|
||||
(defn insert-undos!
|
||||
(mu/defn insert-undos!
|
||||
"Insert the Undo `undos`."
|
||||
[undos]
|
||||
[undos :- [:sequential
|
||||
::action-v2.schema/undo.update]]
|
||||
(t2/insert! :model/Undo undos))
|
||||
|
||||
(defn mark-batch-undone!
|
||||
(mu/defn mark-batch-undone!
|
||||
"Set the undone flag of the Undo batch `batch-num` to `undone?`."
|
||||
[batch-num undone?]
|
||||
[batch-num :- ms/PositiveInt
|
||||
undone? :- :boolean]
|
||||
(t2/update! :model/Undo {:batch_num batch-num} {:undone undone?}))
|
||||
|
||||
(defn superseding-change-exists?
|
||||
(mu/defn superseding-change-exists?
|
||||
"Whether a later (when `undo?`) or earlier Undo row for the rows `row-pks` of the Tables `table-ids` exists beyond
|
||||
batch `batch-num` with the opposite undone state."
|
||||
[undo? table-ids row-pks batch-num]
|
||||
[undo? :- :boolean
|
||||
table-ids :- [:set ::lib.schema.id/table]
|
||||
row-pks :- [:set :some]
|
||||
batch-num :- ms/PositiveInt]
|
||||
(t2/exists? :model/Undo
|
||||
:table_id [:in table-ids]
|
||||
:row_pk [:in row-pks]
|
||||
:batch_num [(if undo? :> :<) batch-num]
|
||||
:undone (not undo?)))
|
||||
|
||||
(defn lock-sequence-next-val
|
||||
(mu/defn lock-sequence-next-val
|
||||
"The next value of the raw sequence row named `sequence-name`, locked for update, or nil."
|
||||
[sequence-name]
|
||||
[sequence-name :- :string]
|
||||
(t2/select-one-fn :next_val [:sequences :next_val] :name sequence-name {:for :update}))
|
||||
|
||||
(defn set-sequence-next-val!
|
||||
(mu/defn set-sequence-next-val!
|
||||
"Set the next value of the raw sequence row named `sequence-name`."
|
||||
[sequence-name next-val]
|
||||
[sequence-name :- :string
|
||||
next-val :- :int]
|
||||
(t2/update! :sequences {:name sequence-name} {:next_val next-val}))
|
||||
|
||||
(defn insert-sequence!
|
||||
(mu/defn insert-sequence!
|
||||
"Insert a raw sequence row named `sequence-name` whose next value is `next-val`."
|
||||
[sequence-name next-val]
|
||||
[sequence-name :- :string
|
||||
next-val :- :int]
|
||||
(t2/insert! :sequences {:name sequence-name :next_val next-val}))
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
(ns metabase-enterprise.action-v2.schema
|
||||
"Malli schemas for the action-v2 module."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
(mr/def ::undo.row-pk
|
||||
"The `:row_pk` column of a Undo, decoded."
|
||||
:map)
|
||||
|
||||
(mr/def ::undo.raw-before
|
||||
"The `:raw_before` column of a Undo, decoded."
|
||||
:map)
|
||||
|
||||
(mr/def ::undo.raw-after
|
||||
"The `:raw_after` column of a Undo, decoded."
|
||||
:map)
|
||||
|
||||
(mr/def ::undo
|
||||
"A Undo as selected from the app DB: every column of `:data_edit_undo_chain`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:batch_num :int]
|
||||
[:table_id ::lib.schema.id/table]
|
||||
[:row_pk ::undo.row-pk]
|
||||
[:user_id ::lib.schema.id/user]
|
||||
[:scope [:or :keyword :string]]
|
||||
[:undoable :boolean]
|
||||
[:raw_before [:maybe ::undo.raw-before]]
|
||||
[:raw_after [:maybe ::undo.raw-after]]
|
||||
[:undone :boolean]
|
||||
[:created_at ms/TemporalInstant]
|
||||
[:updated_at ms/TemporalInstant]])
|
||||
|
||||
(mr/def ::undo.update
|
||||
"What an update (or insert) of a Undo accepts: every column of `:data_edit_undo_chain` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:batch_num {:optional true} [:maybe :int]]
|
||||
[:table_id {:optional true} [:maybe ::lib.schema.id/table]]
|
||||
[:row_pk {:optional true} [:maybe ::undo.row-pk]]
|
||||
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
|
||||
[:scope {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:undoable {:optional true} [:maybe :boolean]]
|
||||
[:raw_before {:optional true} [:maybe ::undo.raw-before]]
|
||||
[:raw_after {:optional true} [:maybe ::undo.raw-after]]
|
||||
[:undone {:optional true} [:maybe :boolean]]
|
||||
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]])
|
||||
@@ -2,12 +2,19 @@
|
||||
"Application database queries for the advanced-config module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so no other namespace in the module runs a query itself."
|
||||
(:require
|
||||
[metabase.api-keys.schema :as api-keys.schema]
|
||||
[metabase.app-db.core :as mdb]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.users.schema :as users.schema]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[metabase.warehouses.schema :as warehouses.schema]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn query-executions-in-month
|
||||
(mu/defn query-executions-in-month
|
||||
"The raw query execution rows started in `month` of `year`, newest first."
|
||||
[year month]
|
||||
[year :- ms/PositiveInt
|
||||
month :- ms/PositiveInt]
|
||||
(let [date-part (fn [part-key part-value]
|
||||
(if (= (mdb/db-type) :postgres)
|
||||
[:= [:date_part ^:allow-raw-sql [:inline (name part-key)] :started_at] part-value]
|
||||
@@ -18,62 +25,67 @@
|
||||
(date-part :year year)
|
||||
(date-part :month month)]})))
|
||||
|
||||
(defn api-key-by-name
|
||||
(mu/defn api-key-by-name
|
||||
"The ApiKey named `api-key-name`, or nil."
|
||||
[api-key-name]
|
||||
[api-key-name :- :string]
|
||||
(t2/select-one :model/ApiKey :name api-key-name))
|
||||
|
||||
(defn api-key-prefix-exists?
|
||||
(mu/defn api-key-prefix-exists?
|
||||
"Whether an ApiKey with `prefix` exists."
|
||||
[prefix]
|
||||
[prefix :- :string]
|
||||
(t2/exists? :model/ApiKey :key_prefix prefix))
|
||||
|
||||
(defn insert-api-key!
|
||||
"Insert `api-key` and return the new instance."
|
||||
[api-key]
|
||||
(mu/defn insert-api-key!
|
||||
"Insert `api-key` and return the new instance. `::api-keys/unhashed-key` has a special meaning to the ApiKey
|
||||
model's before-insert hook: it is hashed into `:key` and used to derive `:key_prefix`."
|
||||
[api-key :- ::api-keys.schema/api-key.create]
|
||||
(t2/insert-returning-instance! :model/ApiKey api-key))
|
||||
|
||||
(defn user-by-email
|
||||
(mu/defn user-by-email
|
||||
"The User with `email`, or nil."
|
||||
[email]
|
||||
[email :- :string]
|
||||
(t2/select-one :model/User :email email))
|
||||
|
||||
(defn user-columns-by-email
|
||||
(mu/defn user-columns-by-email
|
||||
"The `columns` of the User with `email`, or nil."
|
||||
[columns email]
|
||||
[columns :- [:sequential :keyword]
|
||||
email :- :string]
|
||||
(t2/select-one (into [:model/User] columns) :email email))
|
||||
|
||||
(defn insert-user!
|
||||
(mu/defn insert-user!
|
||||
"Insert `user` and return the new instance."
|
||||
[user]
|
||||
[user :- ::users.schema/user.update]
|
||||
(t2/insert-returning-instance! :model/User user))
|
||||
|
||||
(defn update-user!
|
||||
(mu/defn update-user!
|
||||
"Apply `changes` to the User with `user-id`."
|
||||
[user-id changes]
|
||||
[user-id :- ::lib.schema.id/user
|
||||
changes :- ::users.schema/user.update]
|
||||
(t2/update! :model/User user-id changes))
|
||||
|
||||
(defn sample-database-exists?
|
||||
(mu/defn sample-database-exists?
|
||||
"Whether the sample Database exists."
|
||||
[]
|
||||
(t2/exists? :model/Database :is_sample true))
|
||||
|
||||
(defn database-id-by-engine-and-name
|
||||
(mu/defn database-id-by-engine-and-name
|
||||
"The ID of the Database of `engine` named `database-name`, or nil."
|
||||
[engine database-name]
|
||||
[engine :- :string
|
||||
database-name :- :string]
|
||||
(t2/select-one-pk :model/Database :engine engine :name database-name))
|
||||
|
||||
(defn insert-database!
|
||||
(mu/defn insert-database!
|
||||
"Insert `database` and return the new instance."
|
||||
[database]
|
||||
[database :- ::warehouses.schema/database.update]
|
||||
(t2/insert-returning-instance! :model/Database database))
|
||||
|
||||
(defn update-database!
|
||||
(mu/defn update-database!
|
||||
"Apply `changes` to the Database with `database-id`."
|
||||
[database-id changes]
|
||||
[database-id :- ::lib.schema.id/database
|
||||
changes :- ::warehouses.schema/database.update]
|
||||
(t2/update! :model/Database database-id changes))
|
||||
|
||||
(defn delete-database!
|
||||
(mu/defn delete-database!
|
||||
"Delete the Database with `database-id`."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/delete! :model/Database database-id))
|
||||
|
||||
@@ -2,22 +2,27 @@
|
||||
"Application database queries for the advanced-permissions module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so no other namespace in the module runs a query itself."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.honey-sql-2 :as h2x]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn table
|
||||
(mu/defn table
|
||||
"The Table with `table-id`, or nil."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one :model/Table :id table-id))
|
||||
|
||||
(defn group-manager?
|
||||
(mu/defn group-manager?
|
||||
"Whether the User with `user-id` manages the PermissionsGroup with `group-id`."
|
||||
[user-id group-id]
|
||||
[user-id :- ::lib.schema.id/user
|
||||
group-id :- ms/PositiveInt]
|
||||
(t2/select-one-fn :is_group_manager :model/PermissionsGroupMembership :user_id user-id :group_id group-id))
|
||||
|
||||
(defn blocked-database-ids-for-group
|
||||
(mu/defn blocked-database-ids-for-group
|
||||
"The subset of `database-ids` the group with `group-id` is blocked from viewing."
|
||||
[group-id database-ids]
|
||||
[group-id :- ms/PositiveInt
|
||||
database-ids :- [:sequential ::lib.schema.id/database]]
|
||||
(t2/select-fn-set :db_id :model/DataPermissions
|
||||
:perm_type :perms/view-data
|
||||
:perm_value :blocked
|
||||
@@ -25,14 +30,16 @@
|
||||
:db_id [:in database-ids]
|
||||
{:select-distinct [:db_id]}))
|
||||
|
||||
(defn impersonated-database-ids-for-group
|
||||
(mu/defn impersonated-database-ids-for-group
|
||||
"The subset of `database-ids` the group with `group-id` accesses through connection impersonation."
|
||||
[group-id database-ids]
|
||||
[group-id :- ms/PositiveInt
|
||||
database-ids :- [:sequential ::lib.schema.id/database]]
|
||||
(t2/select-fn-set :db_id :model/ConnectionImpersonation :group_id group-id :db_id [:in database-ids]))
|
||||
|
||||
(defn sandboxed-database-ids-for-group
|
||||
(mu/defn sandboxed-database-ids-for-group
|
||||
"The `:db_id` rows of the Databases among `database-ids` the group with `group-id` has a sandbox on."
|
||||
[group-id database-ids]
|
||||
[group-id :- ms/PositiveInt
|
||||
database-ids :- [:sequential ::lib.schema.id/database]]
|
||||
(t2/query {:select [[:t.db_id :db_id]]
|
||||
:from [[(t2/table-name :model/Sandbox) :s]]
|
||||
:join [[(t2/table-name :model/Table) :t] [:= :s.table_id :t.id]]
|
||||
@@ -40,28 +47,29 @@
|
||||
[:= :s.group_id group-id]
|
||||
[:in :t.db_id database-ids]]}))
|
||||
|
||||
(defn blocked-group-ids
|
||||
(mu/defn blocked-group-ids
|
||||
"The subset of `group-ids` blocked from viewing some Database."
|
||||
[group-ids]
|
||||
[group-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select-fn-set :group_id :model/DataPermissions
|
||||
:perm_type :perms/view-data
|
||||
:perm_value :blocked
|
||||
:group_id [:in group-ids]
|
||||
{:select-distinct [:group_id]}))
|
||||
|
||||
(defn impersonated-group-ids
|
||||
(mu/defn impersonated-group-ids
|
||||
"The subset of `group-ids` with a connection impersonation."
|
||||
[group-ids]
|
||||
[group-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select-fn-set :group_id :model/ConnectionImpersonation :group_id [:in group-ids]))
|
||||
|
||||
(defn sandboxed-group-ids
|
||||
(mu/defn sandboxed-group-ids
|
||||
"The subset of `group-ids` with a sandbox."
|
||||
[group-ids]
|
||||
[group-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select-fn-set :group_id :model/Sandbox :group_id [:in group-ids]))
|
||||
|
||||
(defn blocked-group-ids-for-database
|
||||
(mu/defn blocked-group-ids-for-database
|
||||
"The subset of `group-ids` blocked from viewing the Database with `database-id`."
|
||||
[database-id group-ids]
|
||||
[database-id :- ::lib.schema.id/database
|
||||
group-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select-fn-set :group_id :model/DataPermissions
|
||||
:db_id database-id
|
||||
:perm_type :perms/view-data
|
||||
@@ -69,9 +77,10 @@
|
||||
:group_id [:in group-ids]
|
||||
{:select-distinct [:group_id]}))
|
||||
|
||||
(defn sandboxed-group-ids-for-database
|
||||
(mu/defn sandboxed-group-ids-for-database
|
||||
"The `:group_id` rows of the groups among `group-ids` with a sandbox on the Database with `database-id`."
|
||||
[database-id group-ids]
|
||||
[database-id :- ::lib.schema.id/database
|
||||
group-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/query {:select [[:s.group_id :group_id]]
|
||||
:from [[(t2/table-name :model/Sandbox) :s]]
|
||||
:join [[(t2/table-name :model/Table) :t] [:= :t.id :s.table_id]]
|
||||
@@ -79,7 +88,7 @@
|
||||
[:in :s.group_id group-ids]
|
||||
[:= :t.db_id database-id]]}))
|
||||
|
||||
(defn application-permissions
|
||||
(mu/defn application-permissions
|
||||
"The Permissions rows for the root object and every application object."
|
||||
[]
|
||||
(t2/select :model/Permissions
|
||||
@@ -87,12 +96,12 @@
|
||||
[:= :object "/"]
|
||||
[:like :object (h2x/literal "/application/%")]]}))
|
||||
|
||||
(defn user-group-memberships
|
||||
(mu/defn user-group-memberships
|
||||
"The group ID (as `:id`) and manager flag of the memberships of the User with `user-id`."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select [:model/PermissionsGroupMembership [:group_id :id] :is_group_manager] :user_id user-id))
|
||||
|
||||
(defn managed-group-ids
|
||||
(mu/defn managed-group-ids
|
||||
"The IDs of the PermissionsGroups the User with `user-id` manages."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-fn-set :group_id :model/PermissionsGroupMembership :user_id user-id :is_group_manager true))
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
"Application database queries for the agent-api module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.agent-api.schema :as agent-api.schema]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn insert-call-log!
|
||||
(mu/defn insert-call-log!
|
||||
"Insert the AgentApiCallLog `row`."
|
||||
[row]
|
||||
[row :- ::agent-api.schema/agent-api-call-log.update]
|
||||
(t2/insert! :model/AgentApiCallLog row))
|
||||
|
||||
(defn delete-call-logs-created-before!
|
||||
(mu/defn delete-call-logs-created-before!
|
||||
"Delete the AgentApiCallLogs created before `cutoff`, returning the number deleted."
|
||||
[cutoff]
|
||||
[cutoff :- ms/TemporalInstant]
|
||||
(t2/delete! :model/AgentApiCallLog {:where [:< :created_at cutoff]}))
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
"Application database queries for the analytics module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.util.malli :as mu]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn database-with-engine-exists?
|
||||
(mu/defn database-with-engine-exists?
|
||||
"Whether a Database whose engine is one of `engines` exists."
|
||||
[engines]
|
||||
[engines :- [:sequential :keyword]]
|
||||
(t2/exists? :model/Database :engine [:in engines]))
|
||||
|
||||
(defn sandbox-exists?
|
||||
(mu/defn sandbox-exists?
|
||||
"Whether any Sandbox exists."
|
||||
[]
|
||||
(t2/exists? :model/Sandbox))
|
||||
|
||||
@@ -67,8 +67,8 @@
|
||||
;; one transaction so a crash can't commit the database row yet skip the permissions guard below —
|
||||
;; the next boot would see the database and no-op, leaving the stale permissions with no re-run path
|
||||
(t2/with-transaction [_conn]
|
||||
(audit-app.db/insert-database! {:is_audit true
|
||||
:id id
|
||||
(audit-app.db/insert-database! id
|
||||
{:is_audit true
|
||||
:name default-db-name
|
||||
:description "Internal Audit DB used to power metabase analytics."
|
||||
:engine engine
|
||||
|
||||
@@ -2,67 +2,76 @@
|
||||
"Application database queries for the audit-app module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for transactions and connections."
|
||||
(:require
|
||||
[malli.util :as mut]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.queries.schema :as queries.schema]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.warehouse-schema.schema :as warehouse-schema.schema]
|
||||
[metabase.warehouses.schema :as warehouses.schema]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn audit-database
|
||||
(mu/defn audit-database
|
||||
"The audit Database, or nil."
|
||||
[]
|
||||
(t2/select-one :model/Database :is_audit true))
|
||||
|
||||
(defn non-audit-database-named
|
||||
(mu/defn non-audit-database-named
|
||||
"The non-audit Database named `database-name`, or nil."
|
||||
[database-name]
|
||||
[database-name :- :string]
|
||||
(t2/select-one :model/Database :name database-name :is_audit false))
|
||||
|
||||
(defn insert-database!
|
||||
"Insert the Database `row`."
|
||||
[row]
|
||||
(t2/insert! :model/Database row))
|
||||
(mu/defn insert-database!
|
||||
"Insert the Database `row` with the fixed `id`."
|
||||
[id :- ::lib.schema.id/database
|
||||
row :- ::warehouses.schema/database.update]
|
||||
(t2/insert! :model/Database (assoc row :id id)))
|
||||
|
||||
(defn insert-returning-database!
|
||||
(mu/defn insert-returning-database!
|
||||
"Insert the Database `row` and return the new instance."
|
||||
[row]
|
||||
[row :- ::warehouses.schema/database.update]
|
||||
(t2/insert-returning-instance! :model/Database row))
|
||||
|
||||
(defn set-database-engine!
|
||||
(mu/defn set-database-engine!
|
||||
"Set the engine of the Database with `database-id`."
|
||||
[database-id engine]
|
||||
[database-id :- ::lib.schema.id/database
|
||||
engine :- :string]
|
||||
(t2/update! :model/Database database-id {:engine engine}))
|
||||
|
||||
(defn delete-database!
|
||||
(mu/defn delete-database!
|
||||
"Delete the Database with `database-id`."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/delete! :model/Database :id database-id))
|
||||
|
||||
(defn delete-audit-databases!
|
||||
(mu/defn delete-audit-databases!
|
||||
"Delete every audit Database."
|
||||
[]
|
||||
(t2/delete! :model/Database :is_audit true))
|
||||
|
||||
(defn delete-permissions-for-database!
|
||||
(mu/defn delete-permissions-for-database!
|
||||
"Delete the Permissions rows on the Database with `database-id`."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/delete! :model/Permissions {:where [:like :object (str "%/db/" database-id "/%")]}))
|
||||
|
||||
(defn tables-of-database-named
|
||||
(mu/defn tables-of-database-named
|
||||
"The Tables of the Database with `database-id` named one of `table-names`."
|
||||
[database-id table-names]
|
||||
[database-id :- ::lib.schema.id/database
|
||||
table-names :- [:or [:set :string] [:sequential :string]]]
|
||||
(t2/select :model/Table :db_id database-id :name [:in table-names]))
|
||||
|
||||
(defn tables-of-database-in-id-order
|
||||
(mu/defn tables-of-database-in-id-order
|
||||
"The `:id`, `:name`, `:schema`, and `:active` of the Tables of the Database with `database-id`, in ID order."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select [:model/Table :id :name :schema :active] :db_id database-id {:order-by [[:id :asc]]}))
|
||||
|
||||
(defn active-public-table-exists?
|
||||
(mu/defn active-public-table-exists?
|
||||
"Whether the Database with `database-id` has an active Table in the `public` schema."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/exists? :model/Table :db_id database-id :schema "public" :active true))
|
||||
|
||||
(defn table-ids-to-downcase
|
||||
(mu/defn table-ids-to-downcase
|
||||
"The IDs of the Metabase-managed Tables of the Database with `database-id` that have no lower-cased counterpart
|
||||
yet."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(mapv :id
|
||||
(t2/query {:select [:table.id]
|
||||
:from [[(t2/table-name :model/Table) :table]]
|
||||
@@ -82,15 +91,15 @@
|
||||
[:= :table.schema nil]]]
|
||||
[:= :self_table.name [:lower :table.name]]]}]]]})))
|
||||
|
||||
(defn downcase-tables!
|
||||
(mu/defn downcase-tables!
|
||||
"Move the Tables with `table-ids` to the `public` schema and lower-case their names."
|
||||
[table-ids]
|
||||
[table-ids :- [:sequential ::lib.schema.id/table]]
|
||||
(t2/update! :model/Table :id [:in table-ids] {:schema "public" :name [:lower :name]}))
|
||||
|
||||
(defn field-ids-to-downcase
|
||||
(mu/defn field-ids-to-downcase
|
||||
"The IDs of the Fields of the Metabase-managed Tables of the Database with `database-id` that have no
|
||||
lower-cased counterpart yet."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(mapv :id
|
||||
(t2/query {:select [:field.id]
|
||||
:from [[(t2/table-name :model/Field) :field]]
|
||||
@@ -112,24 +121,24 @@
|
||||
[:= :table.schema nil]]]
|
||||
[:= :self_field.name [:lower :field.name]]]}]]]})))
|
||||
|
||||
(defn downcase-fields!
|
||||
(mu/defn downcase-fields!
|
||||
"Lower-case the names of the Fields with `field-ids`."
|
||||
[field-ids]
|
||||
[field-ids :- [:sequential ::lib.schema.id/field]]
|
||||
(t2/update! :model/Field :id [:in field-ids] {:name [:lower :name]}))
|
||||
|
||||
(defn clear-table-schemas!
|
||||
(mu/defn clear-table-schemas!
|
||||
"Clear the schema of the Tables of the Database with `database-id`."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/update! :model/Table {:db_id database-id} {:schema nil}))
|
||||
|
||||
(defn upcase-tables!
|
||||
(mu/defn upcase-tables!
|
||||
"Upper-case the schemas and names of the Tables of the Database with `database-id`."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/update! :model/Table {:db_id database-id} {:schema [:upper :schema] :name [:upper :name]}))
|
||||
|
||||
(defn upcase-fields!
|
||||
(mu/defn upcase-fields!
|
||||
"Upper-case the names of the Fields of the Database with `database-id`."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/update! :model/Field
|
||||
{:table_id
|
||||
[:in
|
||||
@@ -138,74 +147,76 @@
|
||||
:where [:= :db_id database-id]}]}
|
||||
{:name [:upper :name]}))
|
||||
|
||||
(defn update-table!
|
||||
(mu/defn update-table!
|
||||
"Apply `changes` to the Table with `table-id`."
|
||||
[table-id changes]
|
||||
[table-id :- ::lib.schema.id/table
|
||||
changes :- (mut/select-keys ::warehouse-schema.schema/table.update [:active :name :schema :is_defective_duplicate])]
|
||||
(t2/update! :model/Table table-id changes))
|
||||
|
||||
(defn delete-table!
|
||||
(mu/defn delete-table!
|
||||
"Delete the Table with `table-id`."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/delete! :model/Table table-id))
|
||||
|
||||
(defn field-names-of-table
|
||||
(mu/defn field-names-of-table
|
||||
"The `:id` and `:name` of the Fields of the Table with `table-id`."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select [:model/Field :id :name] :table_id table-id))
|
||||
|
||||
(defn card-result-metadata-reducible
|
||||
(mu/defn card-result-metadata-reducible
|
||||
"Reducible raw `:id` and `:result_metadata` rows of the Cards of the Database with `database-id`."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/reducible-select [(t2/table-name :model/Card) :id :result_metadata] :database_id database-id))
|
||||
|
||||
(defn cards-of-table
|
||||
(mu/defn cards-of-table
|
||||
"The Cards on the Table with `table-id`."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select :model/Card :table_id table-id))
|
||||
|
||||
(defn table-ids-referenced-by-cards
|
||||
(mu/defn table-ids-referenced-by-cards
|
||||
"The distinct `:table_id` rows of the Cards on the Tables with `table-ids`."
|
||||
[table-ids]
|
||||
[table-ids :- [:sequential ::lib.schema.id/table]]
|
||||
(t2/query {:select-distinct [:table_id]
|
||||
:from [(t2/table-name :model/Card)]
|
||||
:where [:in :table_id table-ids]}))
|
||||
|
||||
(defn update-card!
|
||||
(mu/defn update-card!
|
||||
"Apply `changes` to the Card with `card-id`."
|
||||
[card-id changes]
|
||||
[card-id :- ::lib.schema.id/card
|
||||
changes :- (mut/select-keys ::queries.schema/card.update [:dataset_query :result_metadata])]
|
||||
(t2/update! :model/Card card-id changes))
|
||||
|
||||
(defn first-superuser
|
||||
(mu/defn first-superuser
|
||||
"The `:id` and `:email` of the oldest superuser, or nil."
|
||||
[]
|
||||
(t2/select-one [:model/User :id :email] :is_superuser true {:order-by [[:id :asc]]}))
|
||||
|
||||
(defn collection-by-entity-id
|
||||
(mu/defn collection-by-entity-id
|
||||
"The Collection with `entity-id`, or nil."
|
||||
[entity-id]
|
||||
[entity-id :- :string]
|
||||
(t2/select-one :model/Collection :entity_id entity-id))
|
||||
|
||||
(defn delete-analytics-collections!
|
||||
(mu/defn delete-analytics-collections!
|
||||
"Delete the Collections of the analytics namespace."
|
||||
[]
|
||||
(t2/delete! :model/Collection :namespace "analytics"))
|
||||
|
||||
(defn delete-pulse-channel-recipients-of-user!
|
||||
(mu/defn delete-pulse-channel-recipients-of-user!
|
||||
"Delete the PulseChannelRecipients of the User with `user-id`."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/delete! :model/PulseChannelRecipient :user_id user-id))
|
||||
|
||||
(defn delete-notification-recipients-of-user!
|
||||
(mu/defn delete-notification-recipients-of-user!
|
||||
"Delete the NotificationRecipients of the User with `user-id`."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/delete! :model/NotificationRecipient :user_id user-id))
|
||||
|
||||
(defn archive-pulses-of-creator!
|
||||
(mu/defn archive-pulses-of-creator!
|
||||
"Archive the unarchived Pulses created by the User with `user-id`."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/update! :model/Pulse {:creator_id user-id, :archived false} {:archived true}))
|
||||
|
||||
(defn deactivate-notifications-of-creator!
|
||||
(mu/defn deactivate-notifications-of-creator!
|
||||
"Deactivate the active Notifications created by the User with `user-id`."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/update! :model/Notification {:creator_id user-id :active true} {:active false}))
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
"Application database queries for the billing module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn active-personal-user-count
|
||||
(mu/defn active-personal-user-count
|
||||
"The number of active personal Users."
|
||||
[]
|
||||
(t2/count :model/User :is_active true :type :personal))
|
||||
|
||||
(defn user-email
|
||||
(mu/defn user-email
|
||||
"The email of the User with `user-id`."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one-fn :email :model/User :id user-id))
|
||||
|
||||
@@ -2,12 +2,30 @@
|
||||
"Application database queries for the cache module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for hydration."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn card-cache-config
|
||||
(def ^:private DurationScope
|
||||
"One `{:model :model-id :rerun-cutoff}` scope, as built by `duration-scopes`."
|
||||
[:map {:closed true}
|
||||
[:model :string]
|
||||
[:model-id ms/PositiveInt]
|
||||
[:rerun-cutoff ms/TemporalInstant]])
|
||||
|
||||
(def ^:private CacheConfigChanges
|
||||
"The keys callers pass to [[update-cache-config!]]."
|
||||
[:map {:closed true}
|
||||
[:next_run_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:invalidated_at {:optional true} ms/TemporalInstant]])
|
||||
|
||||
(mu/defn card-cache-config
|
||||
"The most specific CacheConfig applying to the Card with `card-id` on the Dashboard with `dashboard-id` in the
|
||||
Database with `database-id`, or nil."
|
||||
[card-id dashboard-id database-id]
|
||||
[card-id :- ::lib.schema.id/card
|
||||
dashboard-id :- [:maybe ::lib.schema.id/dashboard]
|
||||
database-id :- [:maybe ::lib.schema.id/database]]
|
||||
(let [qs (for [[i model model-id] [[1 "question" card-id]
|
||||
[2 "dashboard" dashboard-id]
|
||||
[3 "database" database-id]
|
||||
@@ -26,25 +44,27 @@
|
||||
:order-by :ordering
|
||||
:limit [:inline 1]})))
|
||||
|
||||
(defn cards-by-id
|
||||
(mu/defn cards-by-id
|
||||
"A map of ID to Card for `card-ids`."
|
||||
[card-ids]
|
||||
[card-ids :- [:set ::lib.schema.id/card]]
|
||||
(t2/select-pk->fn identity :model/Card :id [:in card-ids]))
|
||||
|
||||
(defn router-database-ids
|
||||
(mu/defn router-database-ids
|
||||
"The subset of `database-ids` that are database routers."
|
||||
[database-ids]
|
||||
[database-ids :- [:set ::lib.schema.id/database]]
|
||||
(t2/select-fn-set :database_id :model/DatabaseRouter :database_id [:in database-ids]))
|
||||
|
||||
(defn duration-cache-configs
|
||||
(mu/defn duration-cache-configs
|
||||
"The duration CacheConfigs that refresh automatically."
|
||||
[]
|
||||
(t2/select :model/CacheConfig :strategy :duration :refresh_automatically true))
|
||||
|
||||
(defn duration-queries-to-rerun
|
||||
(mu/defn duration-queries-to-rerun
|
||||
"The query definitions to rerun for the duration cache `scopes`, each `{:model :model-id :rerun-cutoff}`, counting
|
||||
only executions started after `started-after`; `parameterized?` selects parameterized or plain queries."
|
||||
[scopes started-after parameterized?]
|
||||
[scopes :- [:sequential DurationScope]
|
||||
started-after :- ms/TemporalInstant
|
||||
parameterized? :- :boolean]
|
||||
(t2/select
|
||||
:model/Query
|
||||
{:select [:u.query :u.cache-hash :u.card-id :u.dashboard-id :u.count]
|
||||
@@ -82,10 +102,11 @@
|
||||
:group-by [:q.query_hash :q.query :qc.query_hash :qe.card_id :qe.dashboard_id]}})}
|
||||
:u]]}))
|
||||
|
||||
(defn scheduled-base-query-to-rerun
|
||||
(mu/defn scheduled-base-query-to-rerun
|
||||
"The unparameterized query definition of the Card with `card-id` executed most recently after `started-after`, or
|
||||
nil."
|
||||
[card-id started-after]
|
||||
[card-id :- ::lib.schema.id/card
|
||||
started-after :- ms/TemporalInstant]
|
||||
(t2/select-one :model/Query
|
||||
{:select [:q.query [:qe.card_id :card-id]]
|
||||
:from [[(t2/table-name :model/Query) :q]]
|
||||
@@ -99,9 +120,11 @@
|
||||
:order-by [[:qe.started_at :desc]]
|
||||
:limit 1}))
|
||||
|
||||
(defn scheduled-parameterized-queries-to-rerun
|
||||
(mu/defn scheduled-parameterized-queries-to-rerun
|
||||
"The `limit` most common parameterized query definitions of the Card with `card-id` executed after `rerun-cutoff`."
|
||||
[card-id rerun-cutoff limit]
|
||||
[card-id :- ::lib.schema.id/card
|
||||
rerun-cutoff :- ms/TemporalInstant
|
||||
limit :- ms/PositiveInt]
|
||||
(t2/select :model/Query
|
||||
{:select [:q.query [:qe.card_id :card-id]]
|
||||
:from [[(t2/table-name :model/Query) :q]]
|
||||
@@ -119,26 +142,28 @@
|
||||
[[:min :qe.started_at] :asc]]
|
||||
:limit limit}))
|
||||
|
||||
(defn delete-query-caches!
|
||||
(mu/defn delete-query-caches!
|
||||
"Delete the QueryCache entries with `query-hashes`."
|
||||
[query-hashes]
|
||||
[query-hashes :- [:sequential bytes?]]
|
||||
(t2/delete! :model/QueryCache :query_hash [:in query-hashes]))
|
||||
|
||||
(defn dashboard
|
||||
(mu/defn dashboard
|
||||
"The Dashboard with `dashboard-id`, or nil."
|
||||
[dashboard-id]
|
||||
[dashboard-id :- ::lib.schema.id/dashboard]
|
||||
(t2/select-one :model/Dashboard :id dashboard-id))
|
||||
|
||||
(defn cache-configs-ready-to-run
|
||||
(mu/defn cache-configs-ready-to-run
|
||||
"The CacheConfigs of `strategy` whose next run is unset or due at `now`."
|
||||
[strategy now]
|
||||
[strategy :- :keyword
|
||||
now :- ms/TemporalInstant]
|
||||
(t2/select :model/CacheConfig
|
||||
:strategy strategy
|
||||
{:where [:or
|
||||
[:= :next_run_at nil]
|
||||
[:<= :next_run_at now]]}))
|
||||
|
||||
(defn update-cache-config!
|
||||
(mu/defn update-cache-config!
|
||||
"Apply `changes` to the CacheConfig with `cache-config-id`."
|
||||
[cache-config-id changes]
|
||||
[cache-config-id :- ms/PositiveInt
|
||||
changes :- CacheConfigChanges]
|
||||
(t2/update! :model/CacheConfig {:id cache-config-id} changes))
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
"Application database queries for the content-translation module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[malli.util :as mut]
|
||||
[metabase.content-translation.schema :as content-translation.schema]
|
||||
[metabase.util.malli :as mu]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn delete-all-translations!
|
||||
"Delete every ContentTranslation."
|
||||
(mu/defn delete-all-translations!
|
||||
"Delete every ContentTranslation, returning the number deleted."
|
||||
[]
|
||||
(t2/delete! :model/ContentTranslation))
|
||||
|
||||
(defn insert-translations!
|
||||
"Insert the ContentTranslation `rows`."
|
||||
[rows]
|
||||
(mu/defn insert-translations!
|
||||
"Insert the ContentTranslation `rows`, returning the number inserted."
|
||||
[rows :- [:sequential (mut/select-keys ::content-translation.schema/content-translation.update [:locale :msgid :msgstr])]]
|
||||
(t2/insert! :model/ContentTranslation rows))
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
"Application database queries for the content-verification module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn card-exists?
|
||||
(mu/defn card-exists?
|
||||
"Whether a Card with `id` exists."
|
||||
[id]
|
||||
[id :- ::lib.schema.id/card]
|
||||
(t2/exists? :model/Card id))
|
||||
|
||||
(defn dashboard-exists?
|
||||
(mu/defn dashboard-exists?
|
||||
"Whether a Dashboard with `id` exists."
|
||||
[id]
|
||||
[id :- ::lib.schema.id/dashboard]
|
||||
(t2/exists? :model/Dashboard id))
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"Application database queries for the custom-viz-plugin module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for model definitions and hydration methods."
|
||||
(:require
|
||||
[metabase-enterprise.custom-viz-plugin.schema :as custom-viz-plugin.schema]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(def ^:private non-blob-columns
|
||||
@@ -10,52 +13,53 @@
|
||||
:manifest :metabase_version :bundle_hash :dev_bundle_url
|
||||
:created_at :updated_at])
|
||||
|
||||
(defn non-blob-plugin
|
||||
(mu/defn non-blob-plugin
|
||||
"The CustomVizPlugin with `plugin-id` without its bundle, or nil."
|
||||
[plugin-id]
|
||||
[plugin-id :- ms/PositiveInt]
|
||||
(t2/select-one non-blob-columns :id plugin-id))
|
||||
|
||||
(defn non-blob-plugins
|
||||
(mu/defn non-blob-plugins
|
||||
"Every CustomVizPlugin without its bundle, ordered by display name."
|
||||
[]
|
||||
(t2/select non-blob-columns {:order-by [[:display_name :asc]]}))
|
||||
|
||||
(defn active-enabled-non-blob-plugins
|
||||
(mu/defn active-enabled-non-blob-plugins
|
||||
"The active, enabled CustomVizPlugins without their bundle, ordered by display name."
|
||||
[]
|
||||
(t2/select non-blob-columns :status :active :enabled true {:order-by [[:display_name :asc]]}))
|
||||
|
||||
(defn plugin-by-identifier
|
||||
(mu/defn plugin-by-identifier
|
||||
"The CustomVizPlugin with `identifier`, or nil."
|
||||
[identifier]
|
||||
[identifier :- :string]
|
||||
(t2/select-one :model/CustomVizPlugin :identifier identifier))
|
||||
|
||||
(defn plugin-identifier-exists?
|
||||
(mu/defn plugin-identifier-exists?
|
||||
"Whether a CustomVizPlugin with `identifier` exists."
|
||||
[identifier]
|
||||
[identifier :- :string]
|
||||
(t2/exists? :model/CustomVizPlugin :identifier identifier))
|
||||
|
||||
(defn plugin-bundle
|
||||
(mu/defn plugin-bundle
|
||||
"The bundle bytes of the CustomVizPlugin with `plugin-id`."
|
||||
[plugin-id]
|
||||
[plugin-id :- ms/PositiveInt]
|
||||
(t2/select-one-fn :bundle :model/CustomVizPlugin :id plugin-id))
|
||||
|
||||
(defn plugin-dev-bundle-url
|
||||
(mu/defn plugin-dev-bundle-url
|
||||
"The dev bundle URL of the CustomVizPlugin with `plugin-id`."
|
||||
[plugin-id]
|
||||
[plugin-id :- ms/PositiveInt]
|
||||
(t2/select-one-fn :dev_bundle_url :model/CustomVizPlugin :id plugin-id))
|
||||
|
||||
(defn insert-plugin!
|
||||
(mu/defn insert-plugin!
|
||||
"Insert `plugin` and return the new instance."
|
||||
[plugin]
|
||||
[plugin :- ::custom-viz-plugin.schema/custom-viz-plugin.update]
|
||||
(t2/insert-returning-instance! :model/CustomVizPlugin plugin))
|
||||
|
||||
(defn update-plugin!
|
||||
"Apply `changes` to the CustomVizPlugin with `plugin-id`."
|
||||
[plugin-id changes]
|
||||
(mu/defn update-plugin!
|
||||
"Apply `changes` to the CustomVizPlugin with `plugin-id`, returning the number updated."
|
||||
[plugin-id :- ms/PositiveInt
|
||||
changes :- ::custom-viz-plugin.schema/custom-viz-plugin.update]
|
||||
(t2/update! :model/CustomVizPlugin plugin-id changes))
|
||||
|
||||
(defn delete-plugin!
|
||||
"Delete the CustomVizPlugin with `plugin-id`."
|
||||
[plugin-id]
|
||||
(mu/defn delete-plugin!
|
||||
"Delete the CustomVizPlugin with `plugin-id`, returning the number deleted."
|
||||
[plugin-id :- ms/PositiveInt]
|
||||
(t2/delete! :model/CustomVizPlugin :id plugin-id))
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
(ns metabase-enterprise.custom-viz-plugin.schema
|
||||
"Malli schemas for the custom-viz-plugin module."
|
||||
(:require
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
(mr/def ::custom-viz-plugin.manifest
|
||||
"The `:manifest` column of a CustomVizPlugin, decoded."
|
||||
:map)
|
||||
|
||||
(mr/def ::custom-viz-plugin
|
||||
"A CustomVizPlugin as selected from the app DB: every column of `:custom_viz_plugin`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:identifier :string]
|
||||
[:display_name :string]
|
||||
[:status [:or :keyword :string]]
|
||||
[:error_message [:maybe :string]]
|
||||
[:bundle [:maybe [:or bytes? :string]]]
|
||||
[:bundle_hash [:maybe :string]]
|
||||
[:created_at ms/TemporalInstant]
|
||||
[:updated_at ms/TemporalInstant]
|
||||
[:enabled :boolean]
|
||||
[:icon [:maybe :string]]
|
||||
[:manifest [:maybe ::custom-viz-plugin.manifest]]
|
||||
[:metabase_version [:maybe :string]]
|
||||
[:dev_bundle_url [:maybe :string]]])
|
||||
|
||||
(mr/def ::custom-viz-plugin.update
|
||||
"What an update (or insert) of a CustomVizPlugin accepts: every column of `:custom_viz_plugin` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:identifier {:optional true} [:maybe :string]]
|
||||
[:display_name {:optional true} [:maybe :string]]
|
||||
[:status {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:error_message {:optional true} [:maybe :string]]
|
||||
[:bundle {:optional true} [:maybe [:or bytes? :string]]]
|
||||
[:bundle_hash {:optional true} [:maybe :string]]
|
||||
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:enabled {:optional true} [:maybe :boolean]]
|
||||
[:icon {:optional true} [:maybe :string]]
|
||||
[:manifest {:optional true} [:maybe ::custom-viz-plugin.manifest]]
|
||||
[:metabase_version {:optional true} [:maybe :string]]
|
||||
[:dev_bundle_url {:optional true} [:maybe :string]]])
|
||||
@@ -2,6 +2,9 @@
|
||||
"Application database queries for the data-apps module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for model definitions and hydration methods."
|
||||
(:require
|
||||
[metabase-enterprise.data-apps.schema :as data-apps.schema]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(def ^:private non-blob-columns
|
||||
@@ -10,71 +13,73 @@
|
||||
:bundle_hash :last_synced_sha :last_synced_at :sync_error
|
||||
:created_at :updated_at])
|
||||
|
||||
(defn non-blob-data-app
|
||||
(mu/defn non-blob-data-app
|
||||
"The DataApp with `data-app-id` without its bundle, or nil."
|
||||
[data-app-id]
|
||||
[data-app-id :- ms/PositiveInt]
|
||||
(t2/select-one non-blob-columns :id data-app-id))
|
||||
|
||||
(defn non-blob-data-app-by-slug
|
||||
(mu/defn non-blob-data-app-by-slug
|
||||
"The DataApp named `slug` without its bundle, or nil."
|
||||
[slug]
|
||||
[slug :- :string]
|
||||
(t2/select-one non-blob-columns :name slug))
|
||||
|
||||
(defn enabled-non-blob-data-app-by-slug
|
||||
(mu/defn enabled-non-blob-data-app-by-slug
|
||||
"The enabled DataApp named `slug` without its bundle, or nil."
|
||||
[slug]
|
||||
[slug :- :string]
|
||||
(t2/select-one non-blob-columns :name slug :enabled true))
|
||||
|
||||
(defn non-blob-data-apps
|
||||
(mu/defn non-blob-data-apps
|
||||
"Every DataApp without its bundle, ordered by display name; only the enabled, error-free ones when `available?`."
|
||||
[available?]
|
||||
[available? :- [:maybe :boolean]]
|
||||
(t2/select non-blob-columns
|
||||
(cond-> {:order-by [[:display_name :asc]]}
|
||||
available? (assoc :where [:and
|
||||
[:= :enabled true]
|
||||
[:= :sync_error nil]]))))
|
||||
|
||||
(defn data-app-bundle
|
||||
(mu/defn data-app-bundle
|
||||
"The bundle bytes of the DataApp with `data-app-id`."
|
||||
[data-app-id]
|
||||
[data-app-id :- ms/PositiveInt]
|
||||
(t2/select-one-fn :bundle :model/DataApp :id data-app-id))
|
||||
|
||||
(defn data-apps-sync-info
|
||||
(mu/defn data-apps-sync-info
|
||||
"The sync-relevant columns of every DataApp."
|
||||
[]
|
||||
(t2/select [:model/DataApp :name :display_name :description :allowed_hosts :bundle_path :bundle_hash :sync_error]))
|
||||
|
||||
(defn data-app-exists?
|
||||
(mu/defn data-app-exists?
|
||||
"Whether a DataApp named `slug` exists."
|
||||
[slug]
|
||||
[slug :- :string]
|
||||
(t2/exists? :model/DataApp :name slug))
|
||||
|
||||
(defn insert-data-app!
|
||||
(mu/defn insert-data-app!
|
||||
"Insert the DataApp `row`."
|
||||
[row]
|
||||
[row :- ::data-apps.schema/data-app.update]
|
||||
(t2/insert! :model/DataApp row))
|
||||
|
||||
(defn update-data-app!
|
||||
(mu/defn update-data-app!
|
||||
"Apply `changes` to the DataApp with `data-app-id`."
|
||||
[data-app-id changes]
|
||||
[data-app-id :- ms/PositiveInt
|
||||
changes :- ::data-apps.schema/data-app.update]
|
||||
(t2/update! :model/DataApp :id data-app-id changes))
|
||||
|
||||
(defn update-data-app-by-slug!
|
||||
(mu/defn update-data-app-by-slug!
|
||||
"Apply `changes` to the DataApp named `slug`."
|
||||
[slug changes]
|
||||
[slug :- :string
|
||||
changes :- ::data-apps.schema/data-app.update]
|
||||
(t2/update! :model/DataApp :name slug changes))
|
||||
|
||||
(defn delete-data-app-by-slug!
|
||||
(mu/defn delete-data-app-by-slug!
|
||||
"Delete the DataApp named `slug`, returning the number deleted."
|
||||
[slug]
|
||||
[slug :- :string]
|
||||
(t2/delete! :model/DataApp :name slug))
|
||||
|
||||
(defn delete-data-apps-not-named!
|
||||
(mu/defn delete-data-apps-not-named!
|
||||
"Delete the DataApps whose name is not one of `slugs`, returning the number deleted."
|
||||
[slugs]
|
||||
[slugs :- [:set :string]]
|
||||
(t2/delete! :model/DataApp :name [:not-in slugs]))
|
||||
|
||||
(defn delete-all-data-apps!
|
||||
(mu/defn delete-all-data-apps!
|
||||
"Delete every DataApp, returning the number deleted."
|
||||
[]
|
||||
(t2/delete! :model/DataApp))
|
||||
|
||||
@@ -31,6 +31,24 @@
|
||||
(derive :metabase/model)
|
||||
(derive :hook/timestamped?))
|
||||
|
||||
(defn- default-last-synced-at
|
||||
"A sync that records `last_synced_sha` happened now unless it says otherwise."
|
||||
[row]
|
||||
(cond-> row
|
||||
(and (contains? row :last_synced_sha) (not (contains? row :last_synced_at)))
|
||||
(assoc :last_synced_at (mi/now))))
|
||||
|
||||
(t2/define-before-insert :model/DataApp
|
||||
[data-app]
|
||||
(default-last-synced-at data-app))
|
||||
|
||||
(t2/define-before-update :model/DataApp
|
||||
[data-app]
|
||||
(let [changes (t2/changes data-app)]
|
||||
(cond-> data-app
|
||||
(and (contains? changes :last_synced_sha) (not (contains? changes :last_synced_at)))
|
||||
(assoc :last_synced_at (mi/now)))))
|
||||
|
||||
;; Reads always see `allowed_hosts` as a vector, never nil — a row synced before
|
||||
;; the column existed has NULL until it's re-synced. Guard on `contains?` so
|
||||
;; selects that don't fetch the column (e.g. `select-one-fn :bundle`) are left
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
(ns metabase-enterprise.data-apps.schema
|
||||
"Malli schemas for the data-apps module."
|
||||
(:require
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
(mr/def ::data-app
|
||||
"A DataApp as selected from the app DB: every column of `:data_app`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:name :string]
|
||||
[:display_name :string]
|
||||
[:allowed_hosts [:maybe [:sequential :string]]]
|
||||
[:bundle_path :string]
|
||||
[:bundle [:maybe [:or bytes? :string]]]
|
||||
[:bundle_hash [:maybe :string]]
|
||||
[:last_synced_sha [:maybe :string]]
|
||||
[:last_synced_at [:maybe ms/TemporalInstant]]
|
||||
[:sync_error [:maybe :string]]
|
||||
[:enabled :boolean]
|
||||
[:created_at ms/TemporalInstant]
|
||||
[:updated_at ms/TemporalInstant]
|
||||
[:description [:maybe :string]]])
|
||||
|
||||
(mr/def ::data-app.update
|
||||
"What an update (or insert) of a DataApp accepts: every column of `:data_app` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:name {:optional true} [:maybe :string]]
|
||||
[:display_name {:optional true} [:maybe :string]]
|
||||
[:allowed_hosts {:optional true} [:maybe [:sequential :string]]]
|
||||
[:bundle_path {:optional true} [:maybe :string]]
|
||||
[:bundle {:optional true} [:maybe [:or bytes? :string]]]
|
||||
[:bundle_hash {:optional true} [:maybe :string]]
|
||||
[:last_synced_sha {:optional true} [:maybe :string]]
|
||||
[:last_synced_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:sync_error {:optional true} [:maybe :string]]
|
||||
[:enabled {:optional true} [:maybe :boolean]]
|
||||
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:description {:optional true} [:maybe :string]]])
|
||||
@@ -150,7 +150,6 @@
|
||||
(upsert-by-name! slug (assoc fields
|
||||
:bundle bytes
|
||||
:last_synced_sha sha
|
||||
:last_synced_at :%now
|
||||
:sync_error nil))
|
||||
(app-content-changed? existing fields)))
|
||||
(catch Throwable e
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
"Application database queries for the data-complexity-score module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for model definitions and hydration methods."
|
||||
(:require
|
||||
[java-time.api :as t]
|
||||
[malli.util :as mut]
|
||||
[metabase-enterprise.data-complexity-score.schema :as data-complexity-score.schema]
|
||||
[metabase.app-db.core :as mdb]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.honey-sql-2 :as h2x]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn active-field-counts-by-table
|
||||
(mu/defn active-field-counts-by-table
|
||||
"Rows of `:table_id` and `:field_count` of active Fields for `table-ids`."
|
||||
[table-ids]
|
||||
[table-ids :- [:sequential ::lib.schema.id/table]]
|
||||
(t2/query {:select [:table_id [:%count.* :field_count]]
|
||||
:from [:metabase_field]
|
||||
:where [:and
|
||||
@@ -16,17 +22,17 @@
|
||||
[:in :table_id table-ids]]
|
||||
:group-by [:table_id]}))
|
||||
|
||||
(defn unarchived-measure-names
|
||||
(mu/defn unarchived-measure-names
|
||||
"The Table ID and name of the unarchived Measures on the Tables with `table-ids`."
|
||||
[table-ids]
|
||||
[table-ids :- [:sequential ::lib.schema.id/table]]
|
||||
(t2/select [:model/Measure :table_id :name] :archived false :table_id [:in table-ids]))
|
||||
|
||||
(defn collection
|
||||
(mu/defn collection
|
||||
"The Collection with `collection-id`, or nil."
|
||||
[collection-id]
|
||||
[collection-id :- ::lib.schema.id/collection]
|
||||
(t2/select-one :model/Collection :id collection-id))
|
||||
|
||||
(defn verified-card-ids
|
||||
(mu/defn verified-card-ids
|
||||
"The IDs of the Cards whose most recent moderation review is verified."
|
||||
[]
|
||||
(t2/select-fn-set :moderated_item_id :model/ModerationReview
|
||||
@@ -34,63 +40,66 @@
|
||||
:most_recent true
|
||||
:status "verified"))
|
||||
|
||||
(defn official-collection-ids
|
||||
(mu/defn official-collection-ids
|
||||
"The IDs of the official Collections."
|
||||
[]
|
||||
(t2/select-fn-set :id :model/Collection :authority_level "official"))
|
||||
|
||||
(defn routed-child-database-ids
|
||||
(mu/defn routed-child-database-ids
|
||||
"The IDs of the Databases that are routing destinations."
|
||||
[]
|
||||
(t2/select-fn-set :id :model/Database :router_database_id [:not= nil]))
|
||||
|
||||
(defn universe-cards
|
||||
(mu/defn universe-cards
|
||||
"The ID, name, type, and Collection of the unarchived metric and model Cards outside the Database with
|
||||
`audit-database-id`."
|
||||
[audit-database-id]
|
||||
[audit-database-id :- ::lib.schema.id/database]
|
||||
(t2/select [:model/Card :id :name :type :collection_id :card_schema]
|
||||
:type [:in ["metric" "model"]]
|
||||
:archived false
|
||||
:database_id [:not= audit-database-id]))
|
||||
|
||||
(defn universe-tables
|
||||
(mu/defn universe-tables
|
||||
"The scoring columns of the active Tables outside the Database with `audit-database-id`."
|
||||
[audit-database-id]
|
||||
[audit-database-id :- ::lib.schema.id/database]
|
||||
(t2/select [:model/Table :id :name :collection_id :is_published :visibility_type :db_id :data_layer :data_authority]
|
||||
:active true
|
||||
:db_id [:not= audit-database-id]))
|
||||
|
||||
(defn metabot-by-entity-id
|
||||
(mu/defn metabot-by-entity-id
|
||||
"The Metabot with `entity-id`, or nil."
|
||||
[entity-id]
|
||||
[entity-id :- :string]
|
||||
(t2/select-one :model/Metabot :entity_id entity-id))
|
||||
|
||||
(defn latest-score-entry
|
||||
(mu/defn latest-score-entry
|
||||
"The most recent DataComplexityScore of `source` for `fingerprint`, or nil."
|
||||
[fingerprint source]
|
||||
[fingerprint :- :string
|
||||
source :- :string]
|
||||
(t2/select-one :model/DataComplexityScore :fingerprint fingerprint :source source {:order-by [[:id :desc]]}))
|
||||
|
||||
(defn score-entry
|
||||
(mu/defn score-entry
|
||||
"The DataComplexityScore with `id`, or nil."
|
||||
[id]
|
||||
[id :- ms/PositiveInt]
|
||||
(t2/select-one :model/DataComplexityScore :id id))
|
||||
|
||||
(defn scored-within-hours?
|
||||
(mu/defn scored-within-hours?
|
||||
"Whether a DataComplexityScore of `source` for `fingerprint` was created within the last `hours` hours of
|
||||
database time."
|
||||
[fingerprint source hours]
|
||||
[fingerprint :- :string
|
||||
source :- :string
|
||||
hours :- number?]
|
||||
(t2/exists? :model/DataComplexityScore
|
||||
{:where [:and
|
||||
[:= :fingerprint fingerprint]
|
||||
[:= :source source]
|
||||
[:>= :created_at (h2x/add-interval-honeysql-form (mdb/db-type) :%now (- hours) :hour)]]}))
|
||||
|
||||
(defn insert-score!
|
||||
(mu/defn insert-score!
|
||||
"Insert `score-entry` and return its ID."
|
||||
[score-entry]
|
||||
[score-entry :- (mut/select-keys ::data-complexity-score.schema/data-complexity-score.update [:fingerprint :source :score_data])]
|
||||
(t2/insert-returning-pk! :model/DataComplexityScore score-entry))
|
||||
|
||||
(defn delete-scores-created-before!
|
||||
"Delete the DataComplexityScores created before `cutoff`, returning the number deleted."
|
||||
[cutoff]
|
||||
(t2/delete! :model/DataComplexityScore {:where [:< :created_at cutoff]}))
|
||||
(mu/defn delete-scores-older-than!
|
||||
"Delete the DataComplexityScores created more than `months` months ago, returning the number deleted."
|
||||
[months :- ms/PositiveInt]
|
||||
(t2/delete! :model/DataComplexityScore {:where [:< :created_at (t/minus (t/offset-date-time) (t/months months))]}))
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
(ns metabase-enterprise.data-complexity-score.schema
|
||||
"Malli schemas for the data-complexity-score module."
|
||||
(:require
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
(mr/def ::data-complexity-score.score-data
|
||||
"The `:score_data` column of a DataComplexityScore, decoded."
|
||||
:map)
|
||||
|
||||
(mr/def ::data-complexity-score
|
||||
"A DataComplexityScore as selected from the app DB: every column of `:data_complexity_score`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:fingerprint :string]
|
||||
[:score_data ::data-complexity-score.score-data]
|
||||
[:created_at ms/TemporalInstant]
|
||||
[:source [:or :keyword :string]]])
|
||||
|
||||
(mr/def ::data-complexity-score.update
|
||||
"What an update (or insert) of a DataComplexityScore accepts: every column of `:data_complexity_score` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:fingerprint {:optional true} [:maybe :string]]
|
||||
[:score_data {:optional true} [:maybe ::data-complexity-score.score-data]]
|
||||
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:source {:optional true} [:maybe [:or :keyword :string]]]])
|
||||
@@ -4,12 +4,10 @@
|
||||
[clojurewerkz.quartzite.jobs :as jobs]
|
||||
[clojurewerkz.quartzite.schedule.cron :as cron]
|
||||
[clojurewerkz.quartzite.triggers :as triggers]
|
||||
[java-time.api :as t]
|
||||
[metabase-enterprise.data-complexity-score.db :as data-complexity-score.db]
|
||||
[metabase.task.core :as task]
|
||||
[metabase.util.log :as log])
|
||||
(:import
|
||||
(java.sql Timestamp)
|
||||
(org.quartz DisallowConcurrentExecution)))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
@@ -22,16 +20,10 @@
|
||||
|
||||
(def ^:private retention-months 3)
|
||||
|
||||
(defn- retention-cutoff-timestamp
|
||||
[months]
|
||||
(Timestamp/valueOf ^java.time.LocalDateTime
|
||||
(t/minus (t/local-date-time) (t/months months))))
|
||||
|
||||
(defn- trim-old-complexity-score-data!
|
||||
[]
|
||||
(log/info "Trimming old Data Complexity Score snapshots.")
|
||||
(let [cutoff (retention-cutoff-timestamp retention-months)
|
||||
deleted (data-complexity-score.db/delete-scores-created-before! cutoff)]
|
||||
(let [deleted (data-complexity-score.db/delete-scores-older-than! retention-months)]
|
||||
(log/infof "Data Complexity Score cleanup complete. Deleted %d rows." (or deleted 0))))
|
||||
|
||||
(task/defjob ^{DisallowConcurrentExecution true
|
||||
|
||||
@@ -4,8 +4,17 @@
|
||||
(:require
|
||||
[clojure.string :as str]
|
||||
[metabase.collections.models.collection :as collection]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(def ^:private TableSelectors
|
||||
[:map {:closed true}
|
||||
[:database-ids {:optional true} [:maybe [:or [:set ::lib.schema.id/database] [:sequential ::lib.schema.id/database]]]]
|
||||
[:table-ids {:optional true} [:maybe [:or [:set ::lib.schema.id/table] [:sequential ::lib.schema.id/table]]]]
|
||||
[:schema-ids {:optional true} [:maybe [:or [:set :string] [:sequential :string]]]]])
|
||||
|
||||
(defn- table-selectors-expr
|
||||
"Matches the Tables selected by `database-ids`, `table-ids`, and `schema-ids` (each `\"<db-id>:<schema>\"`)."
|
||||
[{:keys [database-ids table-ids schema-ids]}]
|
||||
@@ -21,11 +30,13 @@
|
||||
[selectors]
|
||||
^:allow-subquery {:select [:id] :from [(t2/table-name :model/Table)] :where (table-selectors-expr selectors)})
|
||||
|
||||
(defn remapped-table-ids-reducible
|
||||
(mu/defn remapped-table-ids-reducible
|
||||
"Reducible `:table_id` rows of the Tables reachable from `tables` through FK remapping Dimensions, from the
|
||||
`input-field` side to the `output-field` side (`:source_field` or `:target_field`), excluding `tables` themselves.
|
||||
`tables` is a set of Table IDs or a selectors map (see [[table-ids-matching-selectors]])."
|
||||
[input-field output-field tables]
|
||||
[input-field :- :keyword
|
||||
output-field :- :keyword
|
||||
tables :- [:or [:set ms/PositiveInt] TableSelectors]]
|
||||
(let [input-table-id (keyword (name input-field) "table_id")
|
||||
output-table-id (keyword (name output-field) "table_id")
|
||||
table-ids (if (map? tables)
|
||||
@@ -48,10 +59,12 @@
|
||||
[:in input-table-id table-ids]
|
||||
not-in-tables]})))
|
||||
|
||||
(defn table-ids-matching-selectors
|
||||
(mu/defn table-ids-matching-selectors
|
||||
"The IDs of the Tables selected by `selectors` (`{:database-ids :table-ids :schema-ids}`) plus, when given, the
|
||||
`extra-table-ids` that are unpublished (`:unpublished` mode) or any of them (`:any` mode)."
|
||||
[selectors extra-table-ids extra-mode]
|
||||
[selectors :- TableSelectors
|
||||
extra-table-ids :- [:maybe [:set ::lib.schema.id/table]]
|
||||
extra-mode :- [:enum :unpublished :any]]
|
||||
(t2/select-pks-set :model/Table
|
||||
{:where (let [selector-expr (table-selectors-expr selectors)]
|
||||
(if (seq extra-table-ids)
|
||||
@@ -60,48 +73,51 @@
|
||||
:any [:in :id extra-table-ids])]
|
||||
selector-expr))}))
|
||||
|
||||
(defn published-table-ids
|
||||
(mu/defn published-table-ids
|
||||
"The IDs of the published Tables among `table-ids`."
|
||||
[table-ids]
|
||||
[table-ids :- [:set ::lib.schema.id/table]]
|
||||
(t2/select-pks-set :model/Table :id [:in table-ids] :is_published true))
|
||||
|
||||
(defn tables
|
||||
(mu/defn tables
|
||||
"The Tables with `table-ids`."
|
||||
[table-ids]
|
||||
[table-ids :- [:set ::lib.schema.id/table]]
|
||||
(t2/select :model/Table :id [:in table-ids]))
|
||||
|
||||
(defn collection
|
||||
(mu/defn collection
|
||||
"The Collection with `collection-id`, or nil."
|
||||
[collection-id]
|
||||
[collection-id :- ::lib.schema.id/collection]
|
||||
(t2/select-one :model/Collection collection-id))
|
||||
|
||||
(defn latest-table-publishing-event
|
||||
(mu/defn latest-table-publishing-event
|
||||
"The most recent publish or unpublish AuditLog event for `table-id`, or nil."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one [:model/AuditLog :timestamp :topic :user_id]
|
||||
:topic [:in [:table-publish :table-unpublish]]
|
||||
:model "Table"
|
||||
:model_id table-id
|
||||
{:order-by [[:timestamp :desc] [:id :desc]]}))
|
||||
|
||||
(defn user-name-and-email
|
||||
(mu/defn user-name-and-email
|
||||
"The id, first name, last name, and email of the User with `user-id`, or nil."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one [:model/User :id :first_name :last_name :email] user-id))
|
||||
|
||||
(defn publish-tables!
|
||||
"Publish the Tables with `table-ids` into the Collection with `collection-id`."
|
||||
[table-ids collection-id]
|
||||
(mu/defn publish-tables!
|
||||
"Publish the Tables with `table-ids` into the Collection with `collection-id`, returning the number updated."
|
||||
[table-ids :- [:set ::lib.schema.id/table]
|
||||
collection-id :- ::lib.schema.id/collection]
|
||||
(t2/update! :model/Table :id [:in table-ids] {:collection_id collection-id, :is_published true}))
|
||||
|
||||
(defn unpublish-tables!
|
||||
"Unpublish the Tables with `table-ids` and detach them from their Collection."
|
||||
[table-ids]
|
||||
(mu/defn unpublish-tables!
|
||||
"Unpublish the Tables with `table-ids` and detach them from their Collection, returning the number updated."
|
||||
[table-ids :- [:set ::lib.schema.id/table]]
|
||||
(t2/update! :model/Table :id [:in table-ids] {:collection_id nil, :is_published false}))
|
||||
|
||||
(defn published-table-visible-to-user?
|
||||
(mu/defn published-table-visible-to-user?
|
||||
"Whether the Table with `table-id` is published in a Collection the User with `user-id` can read."
|
||||
[table-id user-id superuser?]
|
||||
[table-id :- ::lib.schema.id/table
|
||||
user-id :- ::lib.schema.id/user
|
||||
superuser? :- :boolean]
|
||||
(t2/exists? :model/Table
|
||||
{:where [:and
|
||||
[:= :id table-id]
|
||||
@@ -110,7 +126,7 @@
|
||||
:collection_id {} {:current-user-id user-id
|
||||
:is-superuser? superuser?})]}))
|
||||
|
||||
(defn any-published-table-visible?
|
||||
(mu/defn any-published-table-visible?
|
||||
"Whether the current user can read the Collection of any published Table."
|
||||
[]
|
||||
(t2/exists? :model/Table
|
||||
@@ -118,9 +134,9 @@
|
||||
[:= :is_published true]
|
||||
(collection/visible-collection-filter-clause :collection_id)]}))
|
||||
|
||||
(defn published-table-visible-in-database?
|
||||
(mu/defn published-table-visible-in-database?
|
||||
"Whether the current user can read the Collection of any published Table in the Database with `database-id`."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/exists? :model/Table
|
||||
{:where [:and
|
||||
[:= :db_id database-id]
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
"Application database queries for the database-replication module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn database
|
||||
(mu/defn database
|
||||
"The Database with `database-id`, or nil."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select-one :model/Database :id database-id))
|
||||
|
||||
@@ -2,74 +2,82 @@
|
||||
"Application database queries for the database-routing module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for model definitions, hydration methods, and transactions."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.warehouses.schema :as warehouses.schema]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn database
|
||||
(mu/defn database
|
||||
"The Database with `database-id`, or nil."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select-one :model/Database :id database-id))
|
||||
|
||||
(defn database-name
|
||||
(mu/defn database-name
|
||||
"The name of the Database with `database-id`."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select-one-fn :name :model/Database database-id))
|
||||
|
||||
(defn destination-database?
|
||||
(mu/defn destination-database?
|
||||
"Whether the Database with `database-id` is a routing destination."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/exists? :model/Database :id database-id :router_database_id [:not= nil]))
|
||||
|
||||
(defn destination-database-id
|
||||
(mu/defn destination-database-id
|
||||
"The ID of the destination Database of the router with `router-database-id` named `database-name`, or nil."
|
||||
[router-database-id database-name]
|
||||
[router-database-id :- ::lib.schema.id/database
|
||||
database-name :- :string]
|
||||
(t2/select-one-pk :model/Database :router_database_id router-database-id :name database-name))
|
||||
|
||||
(defn destination-name-exists?
|
||||
(mu/defn destination-name-exists?
|
||||
"Whether the router with `router-database-id` already has a destination Database named one of `names`."
|
||||
[router-database-id names]
|
||||
[router-database-id :- ::lib.schema.id/database
|
||||
names :- [:sequential :string]]
|
||||
(t2/exists? :model/Database :router_database_id router-database-id :name [:in names]))
|
||||
|
||||
(defn insert-databases!
|
||||
(mu/defn insert-databases!
|
||||
"Insert the Database `rows` and return the new instances."
|
||||
[rows]
|
||||
[rows :- [:sequential
|
||||
::warehouses.schema/database.update]]
|
||||
(t2/insert-returning-instances! :model/Database rows))
|
||||
|
||||
(defn transform-exists-for-source-database?
|
||||
(mu/defn transform-exists-for-source-database?
|
||||
"Whether a Transform reads from the Database with `database-id`."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/exists? :model/Transform :source_database_id database-id))
|
||||
|
||||
(defn router-exists?
|
||||
(mu/defn router-exists?
|
||||
"Whether the Database with `database-id` is a router."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/exists? :model/DatabaseRouter :database_id database-id))
|
||||
|
||||
(defn router-for-database
|
||||
(mu/defn router-for-database
|
||||
"The DatabaseRouter of the Database with `database-id`, or nil."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select-one :model/DatabaseRouter :database_id database-id))
|
||||
|
||||
(defn router-user-attribute
|
||||
(mu/defn router-user-attribute
|
||||
"The user attribute the router of the Database with `database-id` routes on."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select-one-fn :user_attribute :model/DatabaseRouter :database_id database-id))
|
||||
|
||||
(defn router-user-attributes-by-database
|
||||
(mu/defn router-user-attributes-by-database
|
||||
"A map of Database ID to routing user attribute for `database-ids`."
|
||||
[database-ids]
|
||||
[database-ids :- [:sequential ::lib.schema.id/database]]
|
||||
(t2/select-fn->fn :database_id :user_attribute :model/DatabaseRouter :database_id [:in database-ids]))
|
||||
|
||||
(defn insert-router!
|
||||
(mu/defn insert-router!
|
||||
"Insert a DatabaseRouter for the Database with `database-id` routing on `user-attribute`."
|
||||
[database-id user-attribute]
|
||||
[database-id :- ::lib.schema.id/database
|
||||
user-attribute :- :string]
|
||||
(t2/insert! :model/DatabaseRouter {:database_id database-id :user_attribute user-attribute}))
|
||||
|
||||
(defn update-router-user-attribute!
|
||||
(mu/defn update-router-user-attribute!
|
||||
"Set the routing user attribute of the router of the Database with `database-id`."
|
||||
[database-id user-attribute]
|
||||
[database-id :- ::lib.schema.id/database
|
||||
user-attribute :- :string]
|
||||
(t2/update! :model/DatabaseRouter :database_id database-id {:user_attribute user-attribute}))
|
||||
|
||||
(defn delete-router!
|
||||
(mu/defn delete-router!
|
||||
"Delete the DatabaseRouter of the Database with `database-id`."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/delete! :model/DatabaseRouter :database_id database-id))
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
(ns metabase-enterprise.database-routing.schema
|
||||
"Malli schemas for the database-routing module."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
(mr/def ::database-router
|
||||
"A DatabaseRouter as selected from the app DB: every column of `:db_router`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:database_id ::lib.schema.id/database]
|
||||
[:user_attribute :string]])
|
||||
|
||||
(mr/def ::database-router.update
|
||||
"What an update (or insert) of a DatabaseRouter accepts: every column of `:db_router` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:database_id {:optional true} [:maybe ::lib.schema.id/database]]
|
||||
[:user_attribute {:optional true} [:maybe :string]]])
|
||||
@@ -3,15 +3,49 @@
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for model definitions, hydration methods, and transactions."
|
||||
(:require
|
||||
[clojure.set :as set]
|
||||
[malli.util :as mut]
|
||||
[metabase-enterprise.dependencies.dependency-types :as deps.dependency-types]
|
||||
[metabase-enterprise.dependencies.schema :as dependencies.schema]
|
||||
[metabase.app-db.core :as mdb]
|
||||
[metabase.collections.models.collection :as collection]
|
||||
[metabase.collections.models.collection.root :as collection.root]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.models.interface :as mi]
|
||||
[metabase.permissions.core :as perms]
|
||||
[metabase.queries.schema :as queries.schema]
|
||||
[metabase.util.honey-sql-2 :as h2x]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(def ^:private VisibleOpts
|
||||
"Opts consumed by [[visible-entities-expr]]: the current user (`:user-id`, `:is-superuser?`,
|
||||
`:is-data-analyst?`), plus whether to include archived items (`:include-archived-items`,
|
||||
default `:exclude`)."
|
||||
[:map {:closed true}
|
||||
[:user-id ::lib.schema.id/user]
|
||||
[:is-superuser? {:optional true} [:maybe :boolean]]
|
||||
[:is-data-analyst? {:optional true} [:maybe :boolean]]
|
||||
[:include-archived-items {:optional true} [:enum :exclude :all :only]]])
|
||||
|
||||
(def ^:private EntityType
|
||||
"A dependency-type keyword, or the equivalent string. Every column these entity types are filtered against uses
|
||||
Toucan's keyword transform, which coerces either representation, so callers (and tests) sometimes pass a plain
|
||||
string instead of the keyword."
|
||||
[:or ::deps.dependency-types/dependency-types :string])
|
||||
|
||||
(def ^:private RestrictionSpec
|
||||
"The edge-restriction filter spec consumed by [[edge-restriction-expr]]: nil for no restriction, or a map
|
||||
combining `:visible` ([[VisibleOpts]]), `:broken?`, `:types` (a set of dependency types), and/or
|
||||
`:entity-type` + `:ids` (restricting to specific entities of one type)."
|
||||
[:maybe
|
||||
[:map {:closed true}
|
||||
[:visible {:optional true} VisibleOpts]
|
||||
[:broken? {:optional true} :boolean]
|
||||
[:types {:optional true} [:set EntityType]]
|
||||
[:entity-type {:optional true} EntityType]
|
||||
[:ids {:optional true} [:sequential ::deps.dependency-types/entity-id]]]])
|
||||
|
||||
;;; ------------------------------------------------ Graph edge restrictions ------------------------------------------------
|
||||
;;; The dependency graph is traversed generically over every entity type the module knows about; these private
|
||||
;;; helpers build the permission and type-allowlist fragments the graph and item-list queries below restrict edges
|
||||
@@ -169,13 +203,22 @@
|
||||
1 (first fragments)
|
||||
(into [:and] fragments))))
|
||||
|
||||
(defn dependency-edges
|
||||
(mu/defn dependency-edges
|
||||
"The Dependencies from the entities of type `entity-type` with `entity-ids`, where `src-type`/`src-id` name the
|
||||
Dependency columns identifying those entities and `dst-type`/`dst-id` name the columns identifying the related
|
||||
entities on the other side of the edge, optionally restricted by `destination-restriction` (applied to
|
||||
`dst-type`/`dst-id`) and `source-restriction` (applied to `src-type`/`src-id`) — see [[edge-restriction-expr]] for the
|
||||
filter spec shape."
|
||||
[{:keys [src-type src-id dst-type dst-id entity-type entity-ids destination-restriction source-restriction]}]
|
||||
[{:keys [src-type src-id dst-type dst-id entity-type entity-ids destination-restriction source-restriction]}
|
||||
:- [:map {:closed true}
|
||||
[:src-type [:enum :from_entity_type :to_entity_type]]
|
||||
[:src-id [:enum :from_entity_id :to_entity_id]]
|
||||
[:dst-type [:enum :from_entity_type :to_entity_type]]
|
||||
[:dst-id [:enum :from_entity_id :to_entity_id]]
|
||||
[:entity-type EntityType]
|
||||
[:entity-ids [:or [:set ::deps.dependency-types/entity-id] [:sequential ::deps.dependency-types/entity-id]]]
|
||||
[:destination-restriction RestrictionSpec]
|
||||
[:source-restriction RestrictionSpec]]]
|
||||
(t2/select :model/Dependency
|
||||
{:where (into [:and
|
||||
[:= src-type (name entity-type)]
|
||||
@@ -184,10 +227,12 @@
|
||||
[(edge-restriction-expr dst-type dst-id destination-restriction)
|
||||
(edge-restriction-expr src-type src-id source-restriction)])}))
|
||||
|
||||
(defn finding-errors-from-sources
|
||||
(mu/defn finding-errors-from-sources
|
||||
"The AnalysisFindingErrors caused by any of the entities `source-entity-type` `source-entity-ids`, whose analyzed
|
||||
entity is visible to the user described by `user-id`, `is-superuser?`, and `is-data-analyst?`."
|
||||
[source-entity-type source-entity-ids {:keys [user-id is-superuser? is-data-analyst?]}]
|
||||
[source-entity-type :- EntityType
|
||||
source-entity-ids :- [:sequential ::deps.dependency-types/entity-id]
|
||||
{:keys [user-id is-superuser? is-data-analyst?]} :- VisibleOpts]
|
||||
(t2/select :model/AnalysisFindingError
|
||||
{:where [:and
|
||||
[:= :source_entity_type (name source-entity-type)]
|
||||
@@ -196,10 +241,12 @@
|
||||
{:user-id user-id :is-superuser? is-superuser?
|
||||
:is-data-analyst? is-data-analyst?})]}))
|
||||
|
||||
(defn finding-errors-for-entities-with-visible-sources
|
||||
(mu/defn finding-errors-for-entities-with-visible-sources
|
||||
"The AnalysisFindingErrors analyzing any of the entities `entity-type` `entity-ids`, excluding those whose source
|
||||
entity exists but is not visible to the user described by `user-id`, `is-superuser?`, and `is-data-analyst?`."
|
||||
[entity-type entity-ids {:keys [user-id is-superuser? is-data-analyst?]}]
|
||||
[entity-type :- EntityType
|
||||
entity-ids :- [:sequential ::deps.dependency-types/entity-id]
|
||||
{:keys [user-id is-superuser? is-data-analyst?]} :- VisibleOpts]
|
||||
(t2/select :model/AnalysisFindingError
|
||||
{:where [:and
|
||||
[:= :analyzed_entity_type (name entity-type)]
|
||||
@@ -414,14 +461,30 @@
|
||||
:left-join (build-left-joins join all-joins)
|
||||
:where (into [:and join-filter item-visible-expr] (keep identity) exprs)}))
|
||||
|
||||
(defn dependency-item-ids
|
||||
(def ^:private DependencyItemParams
|
||||
"Params shared by [[dependency-item-ids]] and [[dependency-item-count]]."
|
||||
[:map {:closed true}
|
||||
[:query-type [:enum :unreferenced :breaking]]
|
||||
[:entity-types [:sequential ::deps.dependency-types/dependency-types]]
|
||||
[:card-types {:optional true} [:maybe [:sequential [:or :keyword :string]]]]
|
||||
[:search-text {:optional true} [:maybe :string]]
|
||||
[:include-personal-collections? :boolean]
|
||||
[:sort-column {:optional true} [:maybe [:enum :name :location :dependents-errors :dependents-with-errors]]]
|
||||
[:sort-direction [:enum :asc :desc]]
|
||||
[:offset ms/IntGreaterThanOrEqualToZero]
|
||||
[:limit ms/PositiveInt]
|
||||
[:user-id ::lib.schema.id/user]
|
||||
[:is-superuser? {:optional true} [:maybe :boolean]]
|
||||
[:is-data-analyst? {:optional true} [:maybe :boolean]]])
|
||||
|
||||
(mu/defn dependency-item-ids
|
||||
"A page of `[entity-type entity-id]` pairs for `query-type` (`:unreferenced` or `:breaking`), restricted to
|
||||
`entity-types`, `card-types` (applied only to `:card` entities), `search-text` (nil for none), and
|
||||
`include-personal-collections?`; sorted by `sort-column` (`:name`, `:location`, `:dependents-errors`, or
|
||||
`:dependents-with-errors`) in `sort-direction`, skipping `offset` and returning up to `limit`. Entities are
|
||||
restricted to those visible to the user described by `user-id`, `is-superuser?`, and `is-data-analyst?`; throws
|
||||
when `user-id` is missing."
|
||||
[{:keys [entity-types sort-direction offset limit] :as params}]
|
||||
[{:keys [entity-types sort-direction offset limit] :as params} :- DependencyItemParams]
|
||||
(let [union-query ^:allow-subquery {:union-all (mapv #(dependency-item-select (assoc params :entity-type %))
|
||||
entity-types)}]
|
||||
(->> (t2/query (assoc union-query
|
||||
@@ -430,22 +493,30 @@
|
||||
:limit limit))
|
||||
(map (fn [{:keys [entity_id entity_type]}] [(keyword entity_type) entity_id])))))
|
||||
|
||||
(defn dependency-item-count
|
||||
(mu/defn dependency-item-count
|
||||
"The total count of items matching the same criteria as [[dependency-item-ids]] (ignoring sort direction, offset,
|
||||
and limit); throws when `user-id` is missing."
|
||||
[{:keys [entity-types] :as params}]
|
||||
[{:keys [entity-types] :as params} :- DependencyItemParams]
|
||||
(let [union-query ^:allow-subquery {:union-all (mapv #(dependency-item-select (assoc params :entity-type %))
|
||||
entity-types)}]
|
||||
(-> (t2/query {:select [[:%count.* :total]] :from [[union-query :subquery]]})
|
||||
first
|
||||
:total)))
|
||||
|
||||
(defn broken-entity-pairs
|
||||
(mu/defn broken-entity-pairs
|
||||
"The `[:analyzed_entity_type :analyzed_entity_id]` pairs whose analysis failed and were caused by the entity
|
||||
`source-entity-type` `source-entity-id`, restricted to `dependent-types` and `dependent-card-types` (each nil
|
||||
for no restriction), visible to the user described by `user-id`, `is-superuser?`, and `is-data-analyst?`."
|
||||
[{:keys [source-entity-type source-entity-id dependent-types dependent-card-types
|
||||
user-id is-superuser? is-data-analyst?]}]
|
||||
user-id is-superuser? is-data-analyst?]}
|
||||
:- [:map {:closed true}
|
||||
[:source-entity-type EntityType]
|
||||
[:source-entity-id ms/PositiveInt]
|
||||
[:dependent-types {:optional true} [:maybe [:sequential :string]]]
|
||||
[:dependent-card-types {:optional true} [:maybe [:sequential :string]]]
|
||||
[:user-id ::lib.schema.id/user]
|
||||
[:is-superuser? {:optional true} [:maybe :boolean]]
|
||||
[:is-data-analyst? {:optional true} [:maybe :boolean]]]]
|
||||
(t2/query
|
||||
(cond-> {:select-distinct [[:afe.analyzed_entity_type :entity_type] [:afe.analyzed_entity_id :entity_id]]
|
||||
:from [[:analysis_finding_error :afe]]
|
||||
@@ -472,124 +543,153 @@
|
||||
|
||||
;;; ---------------------------------------------------- Entities ----------------------------------------------------
|
||||
|
||||
(defn instances
|
||||
(mu/defn instances
|
||||
"The instances of the entity type `entity-type` with `ids`."
|
||||
[entity-type ids]
|
||||
[entity-type :- ::deps.dependency-types/dependency-types
|
||||
ids :- [:set ::deps.dependency-types/entity-id]]
|
||||
(t2/select (deps.dependency-types/dependency-type->model entity-type) :id [:in ids]))
|
||||
|
||||
(defn instances-with-columns
|
||||
(mu/defn instances-with-columns
|
||||
"The `columns` of the instances of the entity type `entity-type` with `ids`."
|
||||
[entity-type columns ids]
|
||||
[entity-type :- ::deps.dependency-types/dependency-types
|
||||
columns :- [:sequential :keyword]
|
||||
ids :- [:sequential ::deps.dependency-types/entity-id]]
|
||||
(t2/select (into [(deps.dependency-types/dependency-type->model entity-type)] columns) :id [:in ids]))
|
||||
|
||||
(defn instance-with-columns
|
||||
(mu/defn instance-with-columns
|
||||
"The `columns` of the instance of the entity type `entity-type` with `id`, or nil."
|
||||
[entity-type columns id]
|
||||
[entity-type :- ::deps.dependency-types/dependency-types
|
||||
columns :- [:sequential :keyword]
|
||||
id :- ::deps.dependency-types/entity-id]
|
||||
(t2/select-one (into [(deps.dependency-types/dependency-type->model entity-type)] columns) :id id))
|
||||
|
||||
(defn card
|
||||
(mu/defn card
|
||||
"The Card with `card-id`, or nil."
|
||||
[card-id]
|
||||
[card-id :- ::lib.schema.id/card]
|
||||
(t2/select-one :model/Card :id card-id))
|
||||
|
||||
(defn card-types-by-id
|
||||
(mu/defn card-types-by-id
|
||||
"A map of Card ID to type for `card-ids`."
|
||||
[card-ids]
|
||||
[card-ids :- [:sequential ::lib.schema.id/card]]
|
||||
(t2/select-fn->fn :id :type [:model/Card :id :type :card_schema] :id [:in card-ids]))
|
||||
|
||||
(defn card-database-ids
|
||||
(mu/defn card-database-ids
|
||||
"The `:id`, `:database_id`, and `:card_schema` of the Cards with `card-ids`."
|
||||
[card-ids]
|
||||
[card-ids :- [:sequential ::lib.schema.id/card]]
|
||||
(t2/select [:model/Card :id :database_id :card_schema] :id [:in card-ids]))
|
||||
|
||||
(defn set-card-result-metadata!
|
||||
"Set the result metadata of the Card with `card-id`."
|
||||
[card-id result-metadata]
|
||||
(mu/defn set-card-result-metadata!
|
||||
"Set the result metadata of the Card with `card-id`, returning the number updated."
|
||||
[card-id :- ::lib.schema.id/card
|
||||
result-metadata :- [:maybe ::queries.schema/card.result-metadata]]
|
||||
(t2/update! :model/Card card-id {:result_metadata result-metadata}))
|
||||
|
||||
(defn tables
|
||||
(mu/defn tables
|
||||
"The Tables with `table-ids`."
|
||||
[table-ids]
|
||||
[table-ids :- [:set ::lib.schema.id/table]]
|
||||
(t2/select :model/Table :id [:in table-ids]))
|
||||
|
||||
(defn table-database-ids
|
||||
(mu/defn table-database-ids
|
||||
"The `:id` and `:db_id` of the Tables with `table-ids`."
|
||||
[table-ids]
|
||||
[table-ids :- [:sequential ::lib.schema.id/table]]
|
||||
(t2/select [:model/Table :id :db_id] :id [:in table-ids]))
|
||||
|
||||
(defn table-id-by-name
|
||||
(mu/defn table-id-by-name
|
||||
"The ID of the Table named `table-name` in `schema` of the Database with `db-id`, or nil."
|
||||
[db-id schema table-name]
|
||||
[db-id :- ::lib.schema.id/database
|
||||
schema :- [:maybe :string]
|
||||
table-name :- :string]
|
||||
(t2/select-one-fn :id :model/Table :db_id db-id :schema schema :name table-name))
|
||||
|
||||
(defn transform-sources
|
||||
(mu/defn transform-sources
|
||||
"The `:id` and `:source` of the Transforms with `transform-ids`."
|
||||
[transform-ids]
|
||||
[transform-ids :- [:sequential ::lib.schema.id/transform]]
|
||||
(t2/select [:model/Transform :id :source] :id [:in transform-ids]))
|
||||
|
||||
(defn transform-ids-of-source-database
|
||||
(mu/defn transform-ids-of-source-database
|
||||
"The IDs of the Transforms reading from the Database with `db-id`."
|
||||
[db-id]
|
||||
[db-id :- ::lib.schema.id/database]
|
||||
(t2/select-pks-set :model/Transform :source_database_id db-id))
|
||||
|
||||
;;; -------------------------------------------------- Dependencies --------------------------------------------------
|
||||
|
||||
(defn dependencies-from
|
||||
(mu/defn dependencies-from
|
||||
"The `:id`, `:to_entity_type`, and `:to_entity_id` of the Dependencies of the entity `entity-type` `entity-id`."
|
||||
[entity-type entity-id]
|
||||
[entity-type :- EntityType
|
||||
entity-id :- ms/PositiveInt]
|
||||
(t2/select [:model/Dependency :id :to_entity_type :to_entity_id]
|
||||
:from_entity_type entity-type
|
||||
:from_entity_id entity-id))
|
||||
|
||||
(defn dependency-exists?
|
||||
(mu/defn dependency-exists?
|
||||
"Whether the entity `from-type` `from-id` depends on the entity `to-type` `to-id`."
|
||||
[from-type from-id to-type to-id]
|
||||
[from-type :- EntityType
|
||||
from-id :- ms/PositiveInt
|
||||
to-type :- EntityType
|
||||
to-id :- ms/PositiveInt]
|
||||
(t2/exists? :model/Dependency
|
||||
:from_entity_type from-type :from_entity_id from-id
|
||||
:to_entity_type to-type :to_entity_id to-id))
|
||||
|
||||
(defn insert-dependencies!
|
||||
"Insert the Dependency `rows`."
|
||||
[rows]
|
||||
(mu/defn insert-dependencies!
|
||||
"Insert the Dependency `rows`, returning the number inserted."
|
||||
[rows :- [:sequential [:map {:closed true}
|
||||
[:id {:optional true} ms/PositiveInt]
|
||||
[:from_entity_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:from_entity_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:to_entity_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:to_entity_id {:optional true} [:maybe ms/PositiveInt]]]]]
|
||||
(t2/insert! :model/Dependency rows))
|
||||
|
||||
(defn retarget-dependency!
|
||||
(mu/defn retarget-dependency!
|
||||
"Point the Dependency of the entity `from-type` `from-id` on the entity `old-to-type` `old-to-id` at the entity
|
||||
`new-to-type` `new-to-id`."
|
||||
[from-type from-id old-to-type old-to-id new-to-type new-to-id]
|
||||
`new-to-type` `new-to-id`, returning the number updated."
|
||||
[from-type :- EntityType
|
||||
from-id :- ms/PositiveInt
|
||||
old-to-type :- EntityType
|
||||
old-to-id :- ms/PositiveInt
|
||||
new-to-type :- EntityType
|
||||
new-to-id :- ms/PositiveInt]
|
||||
(t2/update! :model/Dependency
|
||||
{:from_entity_type from-type :from_entity_id from-id
|
||||
:to_entity_type old-to-type :to_entity_id old-to-id}
|
||||
{:to_entity_type new-to-type :to_entity_id new-to-id}))
|
||||
|
||||
(defn delete-dependencies!
|
||||
"Delete the Dependencies with `dependency-ids`."
|
||||
[dependency-ids]
|
||||
(mu/defn delete-dependencies!
|
||||
"Delete the Dependencies with `dependency-ids`, returning the number deleted."
|
||||
[dependency-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/delete! :model/Dependency :id [:in dependency-ids]))
|
||||
|
||||
(defn delete-dependency!
|
||||
"Delete the Dependency of the entity `from-type` `from-id` on the entity `to-type` `to-id`."
|
||||
[from-type from-id to-type to-id]
|
||||
(mu/defn delete-dependency!
|
||||
"Delete the Dependency of the entity `from-type` `from-id` on the entity `to-type` `to-id`, returning the number
|
||||
deleted."
|
||||
[from-type :- EntityType
|
||||
from-id :- ms/PositiveInt
|
||||
to-type :- EntityType
|
||||
to-id :- ms/PositiveInt]
|
||||
(t2/delete! :model/Dependency
|
||||
:from_entity_type from-type :from_entity_id from-id
|
||||
:to_entity_type to-type :to_entity_id to-id))
|
||||
|
||||
(defn delete-dependencies-from!
|
||||
"Delete the Dependencies of the entity `entity-type` `entity-id`."
|
||||
[entity-type entity-id]
|
||||
(mu/defn delete-dependencies-from!
|
||||
"Delete the Dependencies of the entity `entity-type` `entity-id`, returning the number deleted."
|
||||
[entity-type :- EntityType
|
||||
entity-id :- ms/PositiveInt]
|
||||
(t2/delete! :model/Dependency :from_entity_type entity-type :from_entity_id entity-id))
|
||||
|
||||
(defn downstream-table-ids-of-transform
|
||||
(mu/defn downstream-table-ids-of-transform
|
||||
"The IDs of the Tables that depend on the Transform with `transform-id`."
|
||||
[transform-id]
|
||||
[transform-id :- ::lib.schema.id/transform]
|
||||
(t2/select-fn-set :from_entity_id :model/Dependency
|
||||
:from_entity_type :table
|
||||
:to_entity_type :transform
|
||||
:to_entity_id transform-id))
|
||||
|
||||
(defn delete-table-dependencies-on-transform!
|
||||
"Delete the Dependencies of the Tables with `table-ids` on the Transform with `transform-id`."
|
||||
[table-ids transform-id]
|
||||
(mu/defn delete-table-dependencies-on-transform!
|
||||
"Delete the Dependencies of the Tables with `table-ids` on the Transform with `transform-id`, returning the
|
||||
number deleted."
|
||||
[table-ids :- [:sequential ::lib.schema.id/table]
|
||||
transform-id :- ::lib.schema.id/transform]
|
||||
(t2/delete! :model/Dependency
|
||||
:from_entity_type :table
|
||||
:from_entity_id [:in table-ids]
|
||||
@@ -598,20 +698,23 @@
|
||||
|
||||
;;; ------------------------------------------------ Dependency status ------------------------------------------------
|
||||
|
||||
(defn dependency-status
|
||||
(mu/defn dependency-status
|
||||
"The DependencyStatus of the entity `entity-type` `entity-id`, or nil."
|
||||
[entity-type entity-id]
|
||||
[entity-type :- EntityType
|
||||
entity-id :- ms/PositiveInt]
|
||||
(t2/select-one :model/DependencyStatus :entity_type entity-type :entity_id entity-id))
|
||||
|
||||
(defn delete-dependency-status!
|
||||
"Delete the DependencyStatus of the entity `entity-type` `entity-id`."
|
||||
[entity-type entity-id]
|
||||
(mu/defn delete-dependency-status!
|
||||
"Delete the DependencyStatus of the entity `entity-type` `entity-id`, returning the number deleted."
|
||||
[entity-type :- EntityType
|
||||
entity-id :- ms/PositiveInt]
|
||||
(t2/delete! :model/DependencyStatus :entity_type entity-type :entity_id entity-id))
|
||||
|
||||
(defn mark-dependency-status-stale!
|
||||
(mu/defn mark-dependency-status-stale!
|
||||
"Mark the DependencyStatus of `entity-type` `entity-id` as stale for dependency recalculation, creating it if it
|
||||
doesn't exist. Resets retry state so previously-failed entities get a fresh chance."
|
||||
[entity-type entity-id]
|
||||
[entity-type :- EntityType
|
||||
entity-id :- ms/PositiveInt]
|
||||
(mdb/update-or-insert!
|
||||
:model/DependencyStatus
|
||||
{:entity_type entity-type :entity_id entity-id}
|
||||
@@ -620,10 +723,12 @@
|
||||
{:stale true :fail_count 0 :next_retry_at nil :terminal false}
|
||||
{:stale true :dependency_analysis_version 0}))))
|
||||
|
||||
(defn upsert-dependency-status!
|
||||
(mu/defn upsert-dependency-status!
|
||||
"Upsert the DependencyStatus of `entity-type` `entity-id`, setting stale=false, `dependency_analysis_version` to
|
||||
`current-version`, and clearing any failure state."
|
||||
[entity-type entity-id current-version]
|
||||
[entity-type :- EntityType
|
||||
entity-id :- ms/PositiveInt
|
||||
current-version :- ms/PositiveInt]
|
||||
(mdb/update-or-insert!
|
||||
:model/DependencyStatus
|
||||
{:entity_type entity-type :entity_id entity-id}
|
||||
@@ -633,24 +738,29 @@
|
||||
:fail_count 0
|
||||
:next_retry_at nil})))
|
||||
|
||||
(defn record-dependency-status-failure!
|
||||
(mu/defn record-dependency-status-failure!
|
||||
"Upsert the DependencyStatus of `entity-type` `entity-id`, creating it if needed. `update-fn` receives the
|
||||
existing DependencyStatus row (or nil if none exists) and must return the columns to set."
|
||||
[entity-type entity-id update-fn]
|
||||
[entity-type :- EntityType
|
||||
entity-id :- ms/PositiveInt
|
||||
update-fn :- fn?]
|
||||
(mdb/update-or-insert!
|
||||
:model/DependencyStatus
|
||||
{:entity_type entity-type :entity_id entity-id}
|
||||
update-fn))
|
||||
|
||||
(defn pending-retry-exists?
|
||||
(mu/defn pending-retry-exists?
|
||||
"Whether a non-terminal DependencyStatus is waiting for a retry."
|
||||
[]
|
||||
(t2/exists? :model/DependencyStatus :terminal false :next_retry_at [:not= nil]))
|
||||
|
||||
(defn instances-for-dependency-calculation
|
||||
(mu/defn instances-for-dependency-calculation
|
||||
"Up to `batch-size` instances of the entity type `entity-type` without a DependencyStatus, or whose status is
|
||||
stale or below `current-version`, is not terminal, and whose retry time has passed at `now`; stale ones first."
|
||||
[entity-type batch-size current-version now]
|
||||
[entity-type :- ::deps.dependency-types/dependency-types
|
||||
batch-size :- ms/PositiveInt
|
||||
current-version :- ms/PositiveInt
|
||||
now :- ms/TemporalInstant]
|
||||
(let [model (deps.dependency-types/dependency-type->model entity-type)
|
||||
table-name (t2/table-name model)
|
||||
id-field (keyword (name table-name) "id")
|
||||
@@ -676,45 +786,57 @@
|
||||
|
||||
;;; ------------------------------------------------ Analysis findings ------------------------------------------------
|
||||
|
||||
(defn finding-id
|
||||
(mu/defn finding-id
|
||||
"The ID of the AnalysisFinding of the entity `entity-type` `entity-id`, or nil."
|
||||
[entity-type entity-id]
|
||||
[entity-type :- EntityType
|
||||
entity-id :- ms/PositiveInt]
|
||||
(t2/select-one-fn :id [:model/AnalysisFinding :id]
|
||||
:analyzed_entity_type entity-type
|
||||
:analyzed_entity_id entity-id))
|
||||
|
||||
(defn insert-finding!
|
||||
"Insert the AnalysisFinding `row`."
|
||||
[row]
|
||||
(mu/defn insert-finding!
|
||||
"Insert the AnalysisFinding `row`, returning the number inserted."
|
||||
[row :- [:map {:closed true}
|
||||
[:id {:optional true} ms/PositiveInt]
|
||||
[:analyzed_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:analysis_version {:optional true} ms/PositiveInt]
|
||||
[:result {:optional true} :boolean]
|
||||
[:stale {:optional true} :boolean]
|
||||
[:analyzed_entity_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:analyzed_entity_id {:optional true} [:maybe ms/PositiveInt]]]]
|
||||
(t2/insert! :model/AnalysisFinding row))
|
||||
|
||||
(defn update-finding!
|
||||
"Apply `changes` to the AnalysisFinding with `finding-id`."
|
||||
[finding-id changes]
|
||||
(mu/defn update-finding!
|
||||
"Apply `changes` to the AnalysisFinding with `finding-id`, returning the number updated."
|
||||
[finding-id :- ms/PositiveInt
|
||||
changes :- (mut/select-keys ::dependencies.schema/analysis-finding.update [:analyzed_at :analysis_version :result :stale])]
|
||||
(t2/update! :model/AnalysisFinding finding-id changes))
|
||||
|
||||
(defn mark-findings-stale!
|
||||
"Mark the AnalysisFindings of the entities `entity-type` `entity-ids` as stale."
|
||||
[entity-type entity-ids]
|
||||
(mu/defn mark-findings-stale!
|
||||
"Mark the AnalysisFindings of the entities `entity-type` `entity-ids` as stale, returning the number updated."
|
||||
[entity-type :- EntityType
|
||||
entity-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/update! :model/AnalysisFinding
|
||||
:analyzed_entity_type entity-type
|
||||
:analyzed_entity_id [:in entity-ids]
|
||||
{:stale true}))
|
||||
|
||||
(defn stale-finding-exists?
|
||||
(mu/defn stale-finding-exists?
|
||||
"Whether a stale AnalysisFinding exists."
|
||||
[]
|
||||
(t2/exists? :model/AnalysisFinding :stale true))
|
||||
|
||||
(defn stale-finding-count
|
||||
(mu/defn stale-finding-count
|
||||
"The number of stale AnalysisFindings."
|
||||
[]
|
||||
(t2/count :model/AnalysisFinding :stale true))
|
||||
|
||||
(defn instances-for-analysis
|
||||
(mu/defn instances-for-analysis
|
||||
"Up to `batch-size` instances of the entity type `entity-type` whose AnalysisFinding is stale, missing, or below
|
||||
`current-version`; stale ones first, then the longest unanalyzed."
|
||||
[entity-type batch-size current-version]
|
||||
[entity-type :- ::deps.dependency-types/dependency-types
|
||||
batch-size :- ms/PositiveInt
|
||||
current-version :- ms/PositiveInt]
|
||||
(let [model (deps.dependency-types/dependency-type->model entity-type)
|
||||
table-name (t2/table-name model)
|
||||
id-field (keyword (name table-name) "id")
|
||||
@@ -734,10 +856,10 @@
|
||||
[:analysis_finding.analyzed_at :asc]]
|
||||
:limit batch-size})))
|
||||
|
||||
(defn table-ids-with-outdated-findings
|
||||
(mu/defn table-ids-with-outdated-findings
|
||||
"The IDs of the Tables of the Database with `db-id` whose dependents were analyzed before the Table or one of
|
||||
its Fields last changed."
|
||||
[db-id]
|
||||
[db-id :- ::lib.schema.id/database]
|
||||
(t2/select-fn-set :table_id :model/AnalysisFinding
|
||||
{:select [:field_updates/table_id]
|
||||
:from [[^:allow-subquery {:select [[:table/id :table_id]
|
||||
@@ -766,22 +888,25 @@
|
||||
|
||||
;;; --------------------------------------------- Analysis finding errors ---------------------------------------------
|
||||
|
||||
(defn finding-errors-for-entity
|
||||
(mu/defn finding-errors-for-entity
|
||||
"The AnalysisFindingErrors of the entity `entity-type` `entity-id`."
|
||||
[entity-type entity-id]
|
||||
[entity-type :- EntityType
|
||||
entity-id :- ms/PositiveInt]
|
||||
(t2/select :model/AnalysisFindingError :analyzed_entity_type entity-type :analyzed_entity_id entity-id))
|
||||
|
||||
(defn finding-errors-from-source
|
||||
(mu/defn finding-errors-from-source
|
||||
"The AnalysisFindingErrors caused by the entity `source-type` `source-id`."
|
||||
[source-type source-id]
|
||||
[source-type :- [:maybe :metabase.lib.schema.validate/source-entity-type]
|
||||
source-id :- ms/PositiveInt]
|
||||
(t2/select :model/AnalysisFindingError :source_entity_type source-type :source_entity_id source-id))
|
||||
|
||||
(defn insert-finding-errors!
|
||||
"Insert the AnalysisFindingError `rows`."
|
||||
[rows]
|
||||
(mu/defn insert-finding-errors!
|
||||
"Insert the AnalysisFindingError `rows`, returning the number inserted."
|
||||
[rows :- [:sequential (mut/select-keys ::dependencies.schema/analysis-finding-error.update [:analyzed_entity_type :analyzed_entity_id :error_type :error_detail :source_entity_type :source_entity_id])]]
|
||||
(t2/insert! :model/AnalysisFindingError rows))
|
||||
|
||||
(defn delete-finding-errors-for-entity!
|
||||
"Delete the AnalysisFindingErrors of the entity `entity-type` `entity-id`."
|
||||
[entity-type entity-id]
|
||||
(mu/defn delete-finding-errors-for-entity!
|
||||
"Delete the AnalysisFindingErrors of the entity `entity-type` `entity-id`, returning the number deleted."
|
||||
[entity-type :- EntityType
|
||||
entity-id :- ms/PositiveInt]
|
||||
(t2/delete! :model/AnalysisFindingError :analyzed_entity_type entity-type :analyzed_entity_id entity-id))
|
||||
|
||||
@@ -14,6 +14,17 @@
|
||||
(t2/deftransforms :model/AnalysisFinding
|
||||
{:analyzed_entity_type mi/transform-keyword})
|
||||
|
||||
(t2/define-before-insert :model/AnalysisFinding
|
||||
[finding]
|
||||
(merge {:analyzed_at (mi/now)} finding))
|
||||
|
||||
(t2/define-before-update :model/AnalysisFinding
|
||||
[finding]
|
||||
(let [changes (t2/changes finding)]
|
||||
(cond-> finding
|
||||
(and (contains? changes :result) (not (contains? changes :analyzed_at)))
|
||||
(assoc :analyzed_at (mi/now)))))
|
||||
|
||||
(def ^:dynamic *current-analysis-finding-version*
|
||||
"Current version of the query validation logic.
|
||||
This should be incremented when the analysis logic changes.
|
||||
@@ -45,8 +56,7 @@
|
||||
Also writes individual errors to the analysis_finding_error table with source information."
|
||||
[type instance-id result finding-details]
|
||||
(t2/with-transaction [_conn]
|
||||
(let [update {:analyzed_at (mi/now)
|
||||
:analysis_version *current-analysis-finding-version*
|
||||
(let [update {:analysis_version *current-analysis-finding-version*
|
||||
:result result
|
||||
:stale false}
|
||||
existing-id (dependencies.db/finding-id type instance-id)]
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
(:require
|
||||
[metabase.documents.schema :as documents.schema]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli.registry :as mr]))
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
(mr/def ::upstream-deps
|
||||
[:map
|
||||
@@ -14,3 +15,85 @@
|
||||
[:document {:optional true} [:set ::documents.schema/document.id]]
|
||||
[:sandbox {:optional true} [:set ::lib.schema.id/sandbox]]
|
||||
[:segment {:optional true} [:set ::lib.schema.id/segment]]])
|
||||
|
||||
(mr/def ::analysis-finding
|
||||
"A AnalysisFinding as selected from the app DB: every column of `:analysis_finding`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:analyzed_entity_type [:or :keyword :string]]
|
||||
[:analyzed_entity_id ms/PositiveInt]
|
||||
[:analysis_version :int]
|
||||
[:analyzed_at ms/TemporalInstant]
|
||||
[:result :boolean]
|
||||
[:stale :boolean]])
|
||||
|
||||
(mr/def ::analysis-finding.update
|
||||
"What an update (or insert) of a AnalysisFinding accepts: every column of `:analysis_finding` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:analyzed_entity_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:analyzed_entity_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:analysis_version {:optional true} [:maybe :int]]
|
||||
[:analyzed_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:result {:optional true} [:maybe :boolean]]
|
||||
[:stale {:optional true} [:maybe :boolean]]])
|
||||
|
||||
(mr/def ::analysis-finding-error
|
||||
"A AnalysisFindingError as selected from the app DB: every column of `:analysis_finding_error`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:analyzed_entity_type [:or :keyword :string]]
|
||||
[:analyzed_entity_id ms/PositiveInt]
|
||||
[:error_type [:or :keyword :string]]
|
||||
[:error_detail [:maybe :string]]
|
||||
[:source_entity_type [:maybe [:or :keyword :string]]]
|
||||
[:source_entity_id [:maybe ms/PositiveInt]]])
|
||||
|
||||
(mr/def ::analysis-finding-error.update
|
||||
"What an update (or insert) of a AnalysisFindingError accepts: every column of `:analysis_finding_error` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:analyzed_entity_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:analyzed_entity_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:error_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:error_detail {:optional true} [:maybe :string]]
|
||||
[:source_entity_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:source_entity_id {:optional true} [:maybe ms/PositiveInt]]])
|
||||
|
||||
(mr/def ::dependency
|
||||
"A Dependency as selected from the app DB: every column of `:dependency`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:from_entity_type [:or :keyword :string]]
|
||||
[:from_entity_id ms/PositiveInt]
|
||||
[:to_entity_type [:or :keyword :string]]
|
||||
[:to_entity_id ms/PositiveInt]])
|
||||
|
||||
(mr/def ::dependency.update
|
||||
"What an update (or insert) of a Dependency accepts: every column of `:dependency` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:from_entity_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:from_entity_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:to_entity_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:to_entity_id {:optional true} [:maybe ms/PositiveInt]]])
|
||||
|
||||
(mr/def ::dependency-status
|
||||
"A DependencyStatus as selected from the app DB: every column of `:dependency_status`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:entity_type [:or :keyword :string]]
|
||||
[:entity_id ms/PositiveInt]
|
||||
[:dependency_analysis_version :int]
|
||||
[:stale :boolean]
|
||||
[:fail_count :int]
|
||||
[:next_retry_at [:maybe ms/TemporalInstant]]
|
||||
[:terminal :boolean]])
|
||||
|
||||
(mr/def ::dependency-status.update
|
||||
"What an update (or insert) of a DependencyStatus accepts: every column of `:dependency_status` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:entity_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:entity_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:dependency_analysis_version {:optional true} [:maybe :int]]
|
||||
[:stale {:optional true} [:maybe :boolean]]
|
||||
[:fail_count {:optional true} [:maybe :int]]
|
||||
[:next_retry_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:terminal {:optional true} [:maybe :boolean]]])
|
||||
|
||||
@@ -2,32 +2,35 @@
|
||||
"Application database queries for the embedding-hub module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn user-database-exists?
|
||||
(mu/defn user-database-exists?
|
||||
"Whether a non-sample, non-audit Database exists."
|
||||
[]
|
||||
(t2/exists? :model/Database {:where [:and
|
||||
[:= :is_sample false]
|
||||
[:= :is_audit false]]}))
|
||||
|
||||
(defn sample-database-id
|
||||
(mu/defn sample-database-id
|
||||
"The ID of the sample Database, or nil."
|
||||
[]
|
||||
(t2/select-one-pk :model/Database :is_sample true))
|
||||
|
||||
(defn upload-table-exists-in-database?
|
||||
(mu/defn upload-table-exists-in-database?
|
||||
"Whether the Database with `database-id` holds an active uploaded Table."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/exists? :model/Table {:where [:and
|
||||
[:= :active true]
|
||||
[:= :is_upload true]
|
||||
[:= :db_id database-id]]}))
|
||||
|
||||
(defn user-dashboard-exists?
|
||||
(mu/defn user-dashboard-exists?
|
||||
"Whether an unarchived Dashboard exists other than `example-dashboard-id` and outside the Collections with
|
||||
`excluded-collection-ids`."
|
||||
[example-dashboard-id excluded-collection-ids]
|
||||
[example-dashboard-id :- [:maybe ::lib.schema.id/dashboard]
|
||||
excluded-collection-ids :- [:maybe [:sequential ::lib.schema.id/collection]]]
|
||||
(t2/exists? :model/Dashboard {:where (cond-> [:and
|
||||
[:= :archived false]]
|
||||
example-dashboard-id (conj [:not= :id example-dashboard-id])
|
||||
@@ -35,15 +38,15 @@
|
||||
[:is :collection_id nil]
|
||||
[:not-in :collection_id excluded-collection-ids]]))}))
|
||||
|
||||
(defn sandbox-exists?
|
||||
(mu/defn sandbox-exists?
|
||||
"Whether any Sandbox exists."
|
||||
[]
|
||||
(t2/exists? :model/Sandbox))
|
||||
|
||||
(defn user-model-exists?
|
||||
(mu/defn user-model-exists?
|
||||
"Whether an unarchived model Card exists outside the sample Collections and the Collection with
|
||||
`audit-collection-id`."
|
||||
[audit-collection-id]
|
||||
[audit-collection-id :- [:maybe ::lib.schema.id/collection]]
|
||||
(t2/exists? :model/Card {:where [:and
|
||||
[:= :type "model"]
|
||||
[:= :archived false]
|
||||
@@ -58,48 +61,48 @@
|
||||
[:= :sample_coll.id :report_card.collection_id]]}]]]
|
||||
[:is :collection_id nil]]]}))
|
||||
|
||||
(defn active-tenant-exists?
|
||||
(mu/defn active-tenant-exists?
|
||||
"Whether an active Tenant exists."
|
||||
[]
|
||||
(t2/exists? :model/Tenant :is_active true))
|
||||
|
||||
(defn shared-tenant-collection-exists?
|
||||
(mu/defn shared-tenant-collection-exists?
|
||||
"Whether an unarchived shared tenant Collection exists."
|
||||
[]
|
||||
(t2/exists? :model/Collection {:where [:and
|
||||
[:= :namespace "shared-tenant-collection"]
|
||||
[:= :archived false]]}))
|
||||
|
||||
(defn shared-tenant-collection-id
|
||||
(mu/defn shared-tenant-collection-id
|
||||
"The ID of an unarchived shared tenant Collection, or nil."
|
||||
[]
|
||||
(t2/select-one-pk :model/Collection {:where [:and
|
||||
[:= :namespace "shared-tenant-collection"]
|
||||
[:= :archived false]]}))
|
||||
|
||||
(defn unarchived-dashboard-exists-in-collection?
|
||||
(mu/defn unarchived-dashboard-exists-in-collection?
|
||||
"Whether an unarchived Dashboard exists in the Collection with `collection-id`."
|
||||
[collection-id]
|
||||
[collection-id :- ::lib.schema.id/collection]
|
||||
(t2/exists? :model/Dashboard {:where [:and
|
||||
[:= :collection_id collection-id]
|
||||
[:= :archived false]]}))
|
||||
|
||||
(defn impersonation-exists?
|
||||
(mu/defn impersonation-exists?
|
||||
"Whether any ConnectionImpersonation exists."
|
||||
[]
|
||||
(t2/exists? :model/ConnectionImpersonation))
|
||||
|
||||
(defn database-router-exists?
|
||||
(mu/defn database-router-exists?
|
||||
"Whether any DatabaseRouter exists."
|
||||
[]
|
||||
(t2/exists? :model/DatabaseRouter))
|
||||
|
||||
(defn embedded-card-exists?
|
||||
(mu/defn embedded-card-exists?
|
||||
"Whether any Card has embedding enabled."
|
||||
[]
|
||||
(t2/exists? :model/Card :enable_embedding true))
|
||||
|
||||
(defn embedded-dashboard-exists?
|
||||
(mu/defn embedded-dashboard-exists?
|
||||
"Whether any Dashboard has embedding enabled."
|
||||
[]
|
||||
(t2/exists? :model/Dashboard :enable_embedding true))
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
"Application database queries for the entity-retrieval module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn library-cards-in-collections
|
||||
(mu/defn library-cards-in-collections
|
||||
"The ID, name, description, and type of the unarchived metric and model Cards in the Collections with
|
||||
`collection-ids`, optionally narrowed to just `id`."
|
||||
[collection-ids id]
|
||||
[collection-ids :- [:sequential ::lib.schema.id/collection]
|
||||
id :- [:maybe ::lib.schema.id/card]]
|
||||
(t2/select [:model/Card :id :name :description :type :card_schema]
|
||||
{:where [:and
|
||||
[:in :collection_id collection-ids]
|
||||
@@ -15,10 +19,11 @@
|
||||
[:in :type ["metric" "model"]]
|
||||
(when id [:= :id id])]}))
|
||||
|
||||
(defn library-tables-in-collections
|
||||
(mu/defn library-tables-in-collections
|
||||
"The ID, names, and description of the active published Tables in the Collections with `collection-ids`, optionally
|
||||
narrowed to just `id`."
|
||||
[collection-ids id]
|
||||
[collection-ids :- [:sequential ::lib.schema.id/collection]
|
||||
id :- [:maybe ::lib.schema.id/table]]
|
||||
(t2/select [:model/Table :id :name :display_name :description]
|
||||
{:where [:and
|
||||
[:in :collection_id collection-ids]
|
||||
@@ -26,44 +31,47 @@
|
||||
[:= :active true]
|
||||
(when id [:= :id id])]}))
|
||||
|
||||
(defn library-measures-of-tables
|
||||
(mu/defn library-measures-of-tables
|
||||
"The ID, name, and description of the unarchived Measures on the Tables with `table-ids`, optionally narrowed to
|
||||
just `id`."
|
||||
[table-ids id]
|
||||
[table-ids :- [:sequential ::lib.schema.id/table]
|
||||
id :- [:maybe ::lib.schema.id/measure]]
|
||||
(t2/select [:model/Measure :id :name :description]
|
||||
{:where [:and
|
||||
[:in :table_id table-ids]
|
||||
[:= :archived false]
|
||||
(when id [:= :id id])]}))
|
||||
|
||||
(defn library-segments-of-tables
|
||||
(mu/defn library-segments-of-tables
|
||||
"The ID, name, and description of the unarchived Segments on the Tables with `table-ids`, optionally narrowed to
|
||||
just `id`."
|
||||
[table-ids id]
|
||||
[table-ids :- [:sequential ::lib.schema.id/table]
|
||||
id :- [:maybe ::lib.schema.id/segment]]
|
||||
(t2/select [:model/Segment :id :name :description]
|
||||
{:where [:and
|
||||
[:in :table_id table-ids]
|
||||
[:= :archived false]
|
||||
(when id [:= :id id])]}))
|
||||
|
||||
(defn measure-table-id
|
||||
(mu/defn measure-table-id
|
||||
"The Table ID of the Measure with `id`."
|
||||
[id]
|
||||
[id :- ::lib.schema.id/measure]
|
||||
(t2/select-one-fn :table_id :model/Measure :id id))
|
||||
|
||||
(defn segment-table-id
|
||||
(mu/defn segment-table-id
|
||||
"The Table ID of the Segment with `id`."
|
||||
[id]
|
||||
[id :- ::lib.schema.id/segment]
|
||||
(t2/select-one-fn :table_id :model/Segment :id id))
|
||||
|
||||
(defn ai-contexts
|
||||
(mu/defn ai-contexts
|
||||
"The entity type, entity ID, and AI context of every OsiAiContext."
|
||||
[]
|
||||
(t2/select [:model/OsiAiContext :entity_type :entity_local_id :ai_context]))
|
||||
|
||||
(defn ai-context
|
||||
(mu/defn ai-context
|
||||
"The AI context of the entity of `entity-type` with `entity-local-id`."
|
||||
[entity-local-id entity-type]
|
||||
[entity-local-id :- ms/PositiveInt
|
||||
entity-type :- :string]
|
||||
(t2/select-one-fn :ai_context :model/OsiAiContext
|
||||
:entity_local_id entity-local-id
|
||||
:entity_type entity-type))
|
||||
|
||||
@@ -2,33 +2,44 @@
|
||||
"Application database queries for the erd module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for hydration."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.warehouse-schema.models.table :as schema.table]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn active-tables-in-database
|
||||
(def ^:private ActiveTablesOpts
|
||||
"The optional restrictions [[active-tables-in-database]] accepts."
|
||||
[:map {:closed true}
|
||||
[:table-ids {:optional true} [:or [:set ::lib.schema.id/table] [:sequential ::lib.schema.id/table]]]
|
||||
[:schema {:optional true} [:maybe :string]]])
|
||||
|
||||
(mu/defn active-tables-in-database
|
||||
"The `columns` of the active Tables in the Database with `database-id`. `opts` may restrict the result: a
|
||||
`:table-ids` key restricts to those IDs (even when its value is empty), and a `:schema` key restricts to that
|
||||
schema (`\"\"` matches both nil and empty-string schemas)."
|
||||
[columns database-id {:keys [table-ids schema] :as opts}]
|
||||
(t2/select :model/Table
|
||||
{:select columns
|
||||
:where (cond-> [:and [:= :db_id database-id] [:= :active true]]
|
||||
(contains? opts :table-ids) (conj [:in :id table-ids])
|
||||
(contains? opts :schema) (conj (if (= schema "")
|
||||
[:or [:= :schema nil] [:= :schema ""]]
|
||||
[:= :schema schema])))}))
|
||||
[columns :- [:sequential :keyword]
|
||||
database-id :- ::lib.schema.id/database
|
||||
opts :- [:maybe ActiveTablesOpts]]
|
||||
(let [{:keys [table-ids schema]} opts]
|
||||
(t2/select :model/Table
|
||||
{:select columns
|
||||
:where (cond-> [:and [:= :db_id database-id] [:= :active true]]
|
||||
(contains? opts :table-ids) (conj [:in :id table-ids])
|
||||
(contains? opts :schema) (conj (if (= schema "")
|
||||
[:or [:= :schema nil] [:= :schema ""]]
|
||||
[:= :schema schema])))})))
|
||||
|
||||
(defn active-fields-for-tables
|
||||
(mu/defn active-fields-for-tables
|
||||
"The active Fields of the Tables with `table-ids`, in field order."
|
||||
[table-ids]
|
||||
[table-ids :- [:set ::lib.schema.id/table]]
|
||||
(t2/select :model/Field {:where [:and
|
||||
[:in :table_id table-ids]
|
||||
[:= :active true]]
|
||||
:order-by schema.table/field-order-rule}))
|
||||
|
||||
(defn active-fields
|
||||
(mu/defn active-fields
|
||||
"The active Fields with `field-ids`."
|
||||
[field-ids]
|
||||
[field-ids :- [:or [:set ::lib.schema.id/field] [:sequential ::lib.schema.id/field]]]
|
||||
(t2/select :model/Field {:where [:and
|
||||
[:in :id field-ids]
|
||||
[:= :active true]]}))
|
||||
|
||||
@@ -2,24 +2,28 @@
|
||||
"Application database queries for the gsheets module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.warehouses.schema :as warehouses.schema]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn attached-dwh-database-id
|
||||
(mu/defn attached-dwh-database-id
|
||||
"The ID of the attached data warehouse Database, or nil."
|
||||
[]
|
||||
(t2/select-one-fn :id :model/Database :is_attached_dwh true))
|
||||
|
||||
(defn attached-dwh-database
|
||||
(mu/defn attached-dwh-database
|
||||
"The attached data warehouse Database, or nil."
|
||||
[]
|
||||
(t2/select-one :model/Database :is_attached_dwh true))
|
||||
|
||||
(defn setting
|
||||
(mu/defn setting
|
||||
"The Setting with `setting-key`, or nil."
|
||||
[setting-key]
|
||||
[setting-key :- :string]
|
||||
(t2/select-one :model/Setting :key setting-key))
|
||||
|
||||
(defn update-database!
|
||||
"Apply `changes` to the Database with `database-id`."
|
||||
[database-id changes]
|
||||
(mu/defn update-database!
|
||||
"Apply `changes` to the Database with `database-id`, returning the number updated."
|
||||
[database-id :- ::lib.schema.id/database
|
||||
changes :- ::warehouses.schema/database.update]
|
||||
(t2/update! :model/Database database-id changes))
|
||||
|
||||
@@ -2,37 +2,47 @@
|
||||
"Application database queries for the impersonation module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for model definitions and hydration methods."
|
||||
(:require
|
||||
[malli.util :as mut]
|
||||
[metabase-enterprise.impersonation.schema :as impersonation.schema]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn impersonation
|
||||
(mu/defn impersonation
|
||||
"The ConnectionImpersonation with `impersonation-id`, or nil."
|
||||
[impersonation-id]
|
||||
[impersonation-id :- ms/PositiveInt]
|
||||
(t2/select-one :model/ConnectionImpersonation :id impersonation-id))
|
||||
|
||||
(defn impersonation-for-group-and-database
|
||||
(mu/defn impersonation-for-group-and-database
|
||||
"The ConnectionImpersonation of the group with `group-id` on the Database with `database-id`, or nil."
|
||||
[group-id database-id]
|
||||
[group-id :- ms/PositiveInt
|
||||
database-id :- ::lib.schema.id/database]
|
||||
(t2/select-one :model/ConnectionImpersonation :group_id group-id :db_id database-id))
|
||||
|
||||
(defn all-impersonations
|
||||
(mu/defn all-impersonations
|
||||
"Every ConnectionImpersonation, in ID order."
|
||||
[]
|
||||
(t2/select :model/ConnectionImpersonation {:order-by [[:id :asc]]}))
|
||||
|
||||
(defn impersonations-for-groups
|
||||
(mu/defn impersonations-for-groups
|
||||
"The ConnectionImpersonations of the groups with `group-ids`."
|
||||
[group-ids]
|
||||
[group-ids :- [:set ms/PositiveInt]]
|
||||
(t2/select :model/ConnectionImpersonation :group_id [:in group-ids]))
|
||||
|
||||
(defn impersonations-for-groups-and-database
|
||||
(mu/defn impersonations-for-groups-and-database
|
||||
"The ConnectionImpersonations of the groups with `group-ids` on the Database with `database-id`."
|
||||
[group-ids database-id]
|
||||
[group-ids :- [:set ms/PositiveInt]
|
||||
database-id :- ::lib.schema.id/database]
|
||||
(t2/select :model/ConnectionImpersonation :group_id [:in group-ids] :db_id database-id))
|
||||
|
||||
(defn impersonations-matching
|
||||
(mu/defn impersonations-matching
|
||||
"The ConnectionImpersonations narrowed by the optional `database-id`, `group-id`, and `group-ids`, excluding the
|
||||
Database with `excluded-database-id` when given."
|
||||
[database-id group-id group-ids excluded-database-id]
|
||||
[database-id :- [:maybe ::lib.schema.id/database]
|
||||
group-id :- [:maybe ms/PositiveInt]
|
||||
group-ids :- [:maybe [:sequential ms/PositiveInt]]
|
||||
excluded-database-id :- [:maybe ::lib.schema.id/database]]
|
||||
(t2/select :model/ConnectionImpersonation
|
||||
{:where [:and
|
||||
(when database-id [:= :db_id database-id])
|
||||
@@ -40,29 +50,31 @@
|
||||
(when group-ids [:in :group_id group-ids])
|
||||
(when excluded-database-id [:not [:= :db_id excluded-database-id]])]}))
|
||||
|
||||
(defn impersonation-exists-for-database?
|
||||
(mu/defn impersonation-exists-for-database?
|
||||
"Whether the Database with `database-id` has a ConnectionImpersonation."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/exists? :model/ConnectionImpersonation :db_id database-id))
|
||||
|
||||
(defn insert-impersonation!
|
||||
(mu/defn insert-impersonation!
|
||||
"Insert `impersonation` and return the new instance."
|
||||
[impersonation]
|
||||
[impersonation :- (mut/select-keys ::impersonation.schema/connection-impersonation.update [:db_id :group_id :attribute])]
|
||||
(first (t2/insert-returning-instances! :model/ConnectionImpersonation impersonation)))
|
||||
|
||||
(defn delete-impersonation!
|
||||
(mu/defn delete-impersonation!
|
||||
"Delete the ConnectionImpersonation with `impersonation-id`."
|
||||
[impersonation-id]
|
||||
[impersonation-id :- ms/PositiveInt]
|
||||
(t2/delete! :model/ConnectionImpersonation :id impersonation-id))
|
||||
|
||||
(defn delete-impersonations-for-group-and-database!
|
||||
(mu/defn delete-impersonations-for-group-and-database!
|
||||
"Delete the ConnectionImpersonations of the group with `group-id` on the Database with `database-id`."
|
||||
[group-id database-id]
|
||||
[group-id :- ms/PositiveInt
|
||||
database-id :- ::lib.schema.id/database]
|
||||
(t2/delete! :model/ConnectionImpersonation :group_id group-id :db_id database-id))
|
||||
|
||||
(defn view-data-permission-values
|
||||
(mu/defn view-data-permission-values
|
||||
"The set of database-level view-data permission values `group-ids` hold on the Database with `database-id`."
|
||||
[database-id group-ids]
|
||||
[database-id :- ::lib.schema.id/database
|
||||
group-ids :- [:set ms/PositiveInt]]
|
||||
(t2/select-fn-set :perm_value :model/DataPermissions
|
||||
{:where [:and
|
||||
[:= :db_id database-id]
|
||||
@@ -70,12 +82,13 @@
|
||||
[:= :perm_type "perms/view-data"]
|
||||
[:in :group_id group-ids]]}))
|
||||
|
||||
(defn group-ids-for-user
|
||||
"The IDs of the PermissionsGroups the User with `user-id` belongs to."
|
||||
[user-id]
|
||||
(mu/defn group-ids-for-user
|
||||
"The IDs of the PermissionsGroups the User with `user-id` belongs to, or nil when `user-id` is nil (e.g. no
|
||||
current user, such as an internal or unauthenticated context)."
|
||||
[user-id :- [:maybe ::lib.schema.id/user]]
|
||||
(t2/select-fn-set :group_id :model/PermissionsGroupMembership :user_id user-id))
|
||||
|
||||
(defn database
|
||||
(mu/defn database
|
||||
"The Database with `database-id`, or nil."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select-one :model/Database :id database-id))
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
(ns metabase-enterprise.impersonation.schema
|
||||
"Malli schemas for the impersonation module."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
(mr/def ::connection-impersonation
|
||||
"A ConnectionImpersonation as selected from the app DB: every column of `:connection_impersonations`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:db_id ::lib.schema.id/database]
|
||||
[:group_id ms/PositiveInt]
|
||||
[:attribute [:maybe :string]]])
|
||||
|
||||
(mr/def ::connection-impersonation.update
|
||||
"What an update (or insert) of a ConnectionImpersonation accepts: every column of `:connection_impersonations` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:db_id {:optional true} [:maybe ::lib.schema.id/database]]
|
||||
[:group_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:attribute {:optional true} [:maybe :string]]])
|
||||
@@ -3,19 +3,21 @@
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for hydration."
|
||||
(:require
|
||||
[metabase.collections.models.collection :as collection]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn card-types-in-collections
|
||||
(mu/defn card-types-in-collections
|
||||
"The set of Card types present in the Collections with `collection-ids`."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:sequential ::lib.schema.id/collection]]
|
||||
(t2/select-fn-set :type [:model/Card :type] :collection_id [:in collection-ids]))
|
||||
|
||||
(defn published-table-in-collections?
|
||||
(mu/defn published-table-in-collections?
|
||||
"Whether a published Table exists in the Collections with `collection-ids`."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:sequential ::lib.schema.id/collection]]
|
||||
(t2/exists? :model/Table :is_published true :collection_id [:in collection-ids]))
|
||||
|
||||
(defn library-collections
|
||||
(mu/defn library-collections
|
||||
"The readable Library, Library-data, and Library-metrics Collections, ordered by name."
|
||||
[]
|
||||
(t2/select :model/Collection
|
||||
@@ -31,12 +33,12 @@
|
||||
:archive-operation-id nil})]
|
||||
:order-by [[:%lower.name :asc]]}))
|
||||
|
||||
(defn collection-type
|
||||
(mu/defn collection-type
|
||||
"The type of the Collection with `collection-id`."
|
||||
[collection-id]
|
||||
[collection-id :- ::lib.schema.id/collection]
|
||||
(t2/select-one-fn :type [:model/Collection :type] :id collection-id))
|
||||
|
||||
(defn unarchived-card-collection-types-reducible
|
||||
(mu/defn unarchived-card-collection-types-reducible
|
||||
"Reducible distinct Collection ID and Card type pairs of the unarchived Cards."
|
||||
[]
|
||||
(t2/reducible-query {:select-distinct [:collection_id :type]
|
||||
|
||||
@@ -2,39 +2,50 @@
|
||||
"Application database queries for the mcp module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.mcp.schema :as mcp.schema]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn session-log-exists?
|
||||
(mu/defn session-log-exists?
|
||||
"Whether an McpSessionLog with `session-id` exists."
|
||||
[session-id]
|
||||
[session-id :- :string]
|
||||
(t2/exists? :model/McpSessionLog :id session-id))
|
||||
|
||||
(defn session-client-identity
|
||||
(mu/defn session-client-identity
|
||||
"The client name and version of the McpSessionLog with `session-id`, or nil."
|
||||
[session-id]
|
||||
[session-id :- :string]
|
||||
(t2/select-one [:model/McpSessionLog :client_name :client_version] :id session-id))
|
||||
|
||||
(defn insert-session-log!
|
||||
(mu/defn insert-session-log!
|
||||
"Insert the McpSessionLog `row`."
|
||||
[row]
|
||||
[row :- [:map {:closed true}
|
||||
[:id {:optional true} :string]
|
||||
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
|
||||
[:tenant_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:client_name {:optional true} [:maybe :string]]
|
||||
[:client_version {:optional true} [:maybe :string]]
|
||||
[:ip_address {:optional true} [:maybe :string]]
|
||||
[:user_agent {:optional true} [:maybe :string]]]]
|
||||
(t2/insert! :model/McpSessionLog row))
|
||||
|
||||
(defn end-session-log!
|
||||
(mu/defn end-session-log!
|
||||
"Stamp `ended_at` on the McpSessionLog with `session-id`."
|
||||
[session-id]
|
||||
[session-id :- :string]
|
||||
(t2/update! :model/McpSessionLog :id session-id {:ended_at :%now}))
|
||||
|
||||
(defn insert-tool-call-log!
|
||||
(mu/defn insert-tool-call-log!
|
||||
"Insert the McpToolCallLog `row`."
|
||||
[row]
|
||||
[row :- ::mcp.schema/mcp-tool-call-log.update]
|
||||
(t2/insert! :model/McpToolCallLog row))
|
||||
|
||||
(defn delete-tool-call-logs-created-before!
|
||||
(mu/defn delete-tool-call-logs-created-before!
|
||||
"Delete the McpToolCallLogs created before `cutoff`, returning the number deleted."
|
||||
[cutoff]
|
||||
[cutoff :- ms/TemporalInstant]
|
||||
(t2/delete! :model/McpToolCallLog {:where [:< :created_at cutoff]}))
|
||||
|
||||
(defn delete-session-logs-created-before!
|
||||
(mu/defn delete-session-logs-created-before!
|
||||
"Delete the McpSessionLogs created before `cutoff`, returning the number deleted."
|
||||
[cutoff]
|
||||
[cutoff :- ms/TemporalInstant]
|
||||
(t2/delete! :model/McpSessionLog {:where [:< :created_at cutoff]}))
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
"Application database queries for the metabot module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for model definitions, hydration methods, and transactions."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.metabot.schema :as metabot.schema]
|
||||
[metabase.permissions.core :as perms]
|
||||
[metabase.util :as u]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn- default-group-ids
|
||||
@@ -19,19 +23,20 @@
|
||||
[:not-in :group_id (default-group-ids)]
|
||||
[:in :group_id (conj (default-group-ids) (u/the-id (perms/admin-group)))]))
|
||||
|
||||
(defn all-groups
|
||||
(mu/defn all-groups
|
||||
"Every PermissionsGroup, in ID order."
|
||||
[]
|
||||
(t2/select :model/PermissionsGroup {:order-by [[:id :asc]]}))
|
||||
|
||||
(defn all-stored-permissions
|
||||
(mu/defn all-stored-permissions
|
||||
"Every MetabotPermissions row, ordered by group and permission type."
|
||||
[]
|
||||
(t2/select :model/MetabotPermissions {:order-by [[:group_id :asc] [:perm_type :asc]]}))
|
||||
|
||||
(defn visible-permissions-for-user
|
||||
(mu/defn visible-permissions-for-user
|
||||
"The MetabotPermissions rows of the groups of the User with `user-id` that the mode selected by `advanced?` shows."
|
||||
[user-id advanced?]
|
||||
[user-id :- ::lib.schema.id/user
|
||||
advanced? :- :boolean]
|
||||
(t2/select :model/MetabotPermissions
|
||||
{:where [:and
|
||||
[:in :group_id
|
||||
@@ -41,40 +46,47 @@
|
||||
:where [:= :user_id user-id]}]
|
||||
(visible-groups-expr advanced?)]}))
|
||||
|
||||
(defn permission-exists?
|
||||
(mu/defn permission-exists?
|
||||
"Whether the group with `group-id` has a MetabotPermissions row of `perm-type`."
|
||||
[group-id perm-type]
|
||||
[group-id :- ms/PositiveInt
|
||||
perm-type :- :keyword]
|
||||
(t2/exists? :model/MetabotPermissions :group_id group-id :perm_type perm-type))
|
||||
|
||||
(defn update-permission-value!
|
||||
(mu/defn update-permission-value!
|
||||
"Set the value of the MetabotPermissions row of `perm-type` for the group with `group-id`."
|
||||
[group-id perm-type perm-value]
|
||||
[group-id :- ms/PositiveInt
|
||||
perm-type :- :keyword
|
||||
perm-value :- :keyword]
|
||||
(t2/update! :model/MetabotPermissions {:group_id group-id :perm_type perm-type} {:perm_value perm-value}))
|
||||
|
||||
(defn insert-permission!
|
||||
(mu/defn insert-permission!
|
||||
"Insert the MetabotPermissions `row`."
|
||||
[row]
|
||||
[row :- [:map {:closed true}
|
||||
[:id {:optional true} ms/PositiveInt]
|
||||
[:group_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:perm_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:perm_value {:optional true} [:maybe [:or :keyword :string]]]]]
|
||||
(t2/insert! :model/MetabotPermissions row))
|
||||
|
||||
(defn delete-hidden-group-permissions!
|
||||
(mu/defn delete-hidden-group-permissions!
|
||||
"Delete the MetabotPermissions rows of the groups the mode selected by `advanced?` hides."
|
||||
[advanced?]
|
||||
[advanced? :- :boolean]
|
||||
(t2/delete! :model/MetabotPermissions {:where [:not (visible-groups-expr advanced?)]}))
|
||||
|
||||
(defn group-limits
|
||||
(mu/defn group-limits
|
||||
"Every MetabotGroupLimit, in group order."
|
||||
[]
|
||||
(t2/select :model/MetabotGroupLimit {:order-by [[:group_id :asc]]}))
|
||||
|
||||
(defn group-limit
|
||||
(mu/defn group-limit
|
||||
"The MetabotGroupLimit of the group with `group-id`, or nil."
|
||||
[group-id]
|
||||
[group-id :- ms/PositiveInt]
|
||||
(t2/select-one :model/MetabotGroupLimit :group_id group-id))
|
||||
|
||||
(defn max-usage-for-user
|
||||
(mu/defn max-usage-for-user
|
||||
"The `:max_usage` row holding the largest group limit of the User with `user-id`, or nil if any of their groups
|
||||
is unlimited."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/query-one {:select [[[:case
|
||||
[:= [[:count :*]] [[:count :gl.max_usage]]]
|
||||
[[:max :gl.max_usage]]]
|
||||
@@ -83,49 +95,57 @@
|
||||
:left-join [[:metabot_group_limit :gl] [:= :pgm.group_id :gl.group_id]]
|
||||
:where [:= :pgm.user_id user-id]}))
|
||||
|
||||
(defn insert-group-limit!
|
||||
(mu/defn insert-group-limit!
|
||||
"Insert the MetabotGroupLimit `row`."
|
||||
[row]
|
||||
[row :- [:map {:closed true}
|
||||
[:id {:optional true} ms/PositiveInt]
|
||||
[:group_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:max_usage {:optional true} [:maybe :int]]]]
|
||||
(t2/insert! :model/MetabotGroupLimit row))
|
||||
|
||||
(defn update-group-limit!
|
||||
(mu/defn update-group-limit!
|
||||
"Set the maximum usage of the MetabotGroupLimit with `limit-id`."
|
||||
[limit-id max-usage]
|
||||
[limit-id :- ms/PositiveInt
|
||||
max-usage :- [:maybe :int]]
|
||||
(t2/update! :model/MetabotGroupLimit limit-id {:max_usage max-usage}))
|
||||
|
||||
(defn delete-group-limit!
|
||||
(mu/defn delete-group-limit!
|
||||
"Delete the MetabotGroupLimit of the group with `group-id`."
|
||||
[group-id]
|
||||
[group-id :- ms/PositiveInt]
|
||||
(t2/delete! :model/MetabotGroupLimit :group_id group-id))
|
||||
|
||||
(defn instance-limit
|
||||
(mu/defn instance-limit
|
||||
"The MetabotInstanceLimit of the Tenant with `tenant-id` (nil for the instance-wide limit), or nil."
|
||||
[tenant-id]
|
||||
[tenant-id :- [:maybe ms/PositiveInt]]
|
||||
(t2/select-one :model/MetabotInstanceLimit :tenant_id tenant-id))
|
||||
|
||||
(defn tenant-limits
|
||||
(mu/defn tenant-limits
|
||||
"The MetabotInstanceLimits of tenants, ordered by tenant."
|
||||
[]
|
||||
(t2/select :model/MetabotInstanceLimit :tenant_id [:not= nil] {:order-by [[:tenant_id :asc]]}))
|
||||
|
||||
(defn insert-instance-limit!
|
||||
(mu/defn insert-instance-limit!
|
||||
"Insert the MetabotInstanceLimit `row`."
|
||||
[row]
|
||||
[row :- [:map {:closed true}
|
||||
[:id {:optional true} ms/PositiveInt]
|
||||
[:tenant_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:max_usage {:optional true} [:maybe :int]]]]
|
||||
(t2/insert! :model/MetabotInstanceLimit row))
|
||||
|
||||
(defn update-instance-limit!
|
||||
(mu/defn update-instance-limit!
|
||||
"Set the maximum usage of the MetabotInstanceLimit with `limit-id`."
|
||||
[limit-id max-usage]
|
||||
[limit-id :- ms/PositiveInt
|
||||
max-usage :- [:maybe :int]]
|
||||
(t2/update! :model/MetabotInstanceLimit limit-id {:max_usage max-usage}))
|
||||
|
||||
(defn delete-instance-limit!
|
||||
(mu/defn delete-instance-limit!
|
||||
"Delete the MetabotInstanceLimit of the Tenant with `tenant-id`."
|
||||
[tenant-id]
|
||||
[tenant-id :- [:maybe ms/PositiveInt]]
|
||||
(t2/delete! :model/MetabotInstanceLimit :tenant_id tenant-id))
|
||||
|
||||
(defn insert-usage-log!
|
||||
(mu/defn insert-usage-log!
|
||||
"Insert the AiUsageLog `row`."
|
||||
[row]
|
||||
[row :- ::metabot.schema/ai-usage-log.update]
|
||||
(t2/insert! :model/AiUsageLog row))
|
||||
|
||||
(defn- usage-window-expr
|
||||
@@ -135,36 +155,40 @@
|
||||
(when user-id [:= :user_id user-id])
|
||||
(when tenant-id [:= :tenant_id tenant-id])])
|
||||
|
||||
(defn usage-token-sum
|
||||
(mu/defn usage-token-sum
|
||||
"The `:sum` row of tokens logged since `period-start`, narrowed by the optional `user-id` and `tenant-id`."
|
||||
[period-start user-id tenant-id]
|
||||
[period-start :- ms/TemporalInstant
|
||||
user-id :- [:maybe ::lib.schema.id/user]
|
||||
tenant-id :- [:maybe ms/PositiveInt]]
|
||||
(t2/query-one {:select [[[:sum :total_tokens] :sum]]
|
||||
:from [:ai_usage_log]
|
||||
:where (usage-window-expr period-start user-id tenant-id)}))
|
||||
|
||||
(defn usage-message-count
|
||||
(mu/defn usage-message-count
|
||||
"The `:cnt` row of messages logged since `period-start`, narrowed by the optional `user-id` and `tenant-id`."
|
||||
[period-start user-id tenant-id]
|
||||
[period-start :- ms/TemporalInstant
|
||||
user-id :- [:maybe ::lib.schema.id/user]
|
||||
tenant-id :- [:maybe ms/PositiveInt]]
|
||||
(t2/query-one {:select [[[:count :*] :cnt]]
|
||||
:from [:ai_usage_log]
|
||||
:where (usage-window-expr period-start user-id tenant-id)}))
|
||||
|
||||
(defn delete-usage-logs-created-before!
|
||||
(mu/defn delete-usage-logs-created-before!
|
||||
"Delete the AiUsageLogs created before `cutoff`, returning the number deleted."
|
||||
[cutoff]
|
||||
[cutoff :- ms/TemporalInstant]
|
||||
(t2/delete! :model/AiUsageLog {:where [:< :created_at cutoff]}))
|
||||
|
||||
(defn transform
|
||||
(mu/defn transform
|
||||
"The Transform with `transform-id`, or nil."
|
||||
[transform-id]
|
||||
[transform-id :- ::lib.schema.id/transform]
|
||||
(t2/select-one :model/Transform :id transform-id))
|
||||
|
||||
(defn transforms
|
||||
(mu/defn transforms
|
||||
"The Transforms with `transform-ids`."
|
||||
[transform-ids]
|
||||
[transform-ids :- [:sequential ::lib.schema.id/transform]]
|
||||
(t2/select :model/Transform :id [:in transform-ids]))
|
||||
|
||||
(defn cards
|
||||
(mu/defn cards
|
||||
"The Cards with `card-ids`."
|
||||
[card-ids]
|
||||
[card-ids :- [:sequential ::lib.schema.id/card]]
|
||||
(t2/select :model/Card :id [:in card-ids]))
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
(ns metabase-enterprise.metabot.schema
|
||||
"Malli schemas for the metabot module."
|
||||
(:require
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
(mr/def ::metabot-group-limit
|
||||
"A MetabotGroupLimit as selected from the app DB: every column of `:metabot_group_limit`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:group_id ms/PositiveInt]
|
||||
[:max_usage :int]])
|
||||
|
||||
(mr/def ::metabot-group-limit.update
|
||||
"What an update (or insert) of a MetabotGroupLimit accepts: every column of `:metabot_group_limit` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:group_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:max_usage {:optional true} [:maybe :int]]])
|
||||
|
||||
(mr/def ::metabot-instance-limit
|
||||
"A MetabotInstanceLimit as selected from the app DB: every column of `:metabot_instance_limit`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:tenant_id [:maybe ms/PositiveInt]]
|
||||
[:max_usage [:maybe :int]]])
|
||||
|
||||
(mr/def ::metabot-instance-limit.update
|
||||
"What an update (or insert) of a MetabotInstanceLimit accepts: every column of `:metabot_instance_limit` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:tenant_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:max_usage {:optional true} [:maybe :int]]])
|
||||
|
||||
(mr/def ::metabot-permissions
|
||||
"A MetabotPermissions as selected from the app DB: every column of `:metabot_permissions`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:group_id ms/PositiveInt]
|
||||
[:perm_type [:or :keyword :string]]
|
||||
[:perm_value [:or :keyword :string]]])
|
||||
|
||||
(mr/def ::metabot-permissions.update
|
||||
"What an update (or insert) of a MetabotPermissions accepts: every column of `:metabot_permissions` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:group_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:perm_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:perm_value {:optional true} [:maybe [:or :keyword :string]]]])
|
||||
@@ -2,15 +2,18 @@
|
||||
"Application database queries for the metabot-analytics module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for hydration."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.permissions.core :as perms]
|
||||
[metabase.query-processor.parameters.dates :as qp.parameters.dates]
|
||||
[metabase.util.date-2 :as u.date]
|
||||
[metabase.util.i18n :refer [tru]]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn conversation
|
||||
(mu/defn conversation
|
||||
"The MetabotConversation with `conversation-id`, or nil."
|
||||
[conversation-id]
|
||||
[conversation-id :- :string]
|
||||
(t2/select-one :model/MetabotConversation :id conversation-id))
|
||||
|
||||
(defn- date-range-expr
|
||||
@@ -104,11 +107,20 @@
|
||||
[:core_user :u] [:= :u.id :c.user_id]]
|
||||
:group-by [:c.id]})
|
||||
|
||||
(defn list-conversations
|
||||
(mu/defn list-conversations
|
||||
"A page of conversation summary rows (see [[conversation-list-select]]), restricted to `user-id`, `group-id`,
|
||||
`tenant-id`, and `date` (each nil for no restriction), sorted by the allow-listed `sort-by` column name in
|
||||
`sort-direction` (`:asc` or `:desc`), skipping `offset` and returning up to `limit`."
|
||||
[{:keys [user-id group-id tenant-id date sort-by sort-direction offset limit]}]
|
||||
[{:keys [user-id group-id tenant-id date sort-by sort-direction offset limit]}
|
||||
:- [:map {:closed true}
|
||||
[:user-id {:optional true} [:maybe ::lib.schema.id/user]]
|
||||
[:group-id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:tenant-id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:date {:optional true} [:maybe :string]]
|
||||
[:sort-by {:optional true} [:maybe :string]]
|
||||
[:sort-direction {:optional true} [:enum :asc :desc]]
|
||||
[:offset {:optional true} ms/IntGreaterThanOrEqualToZero]
|
||||
[:limit {:optional true} ms/PositiveInt]]]
|
||||
(let [where (conversation-list-expr {:user-id user-id :group-id group-id :tenant-id tenant-id :date date})
|
||||
sort-exprs (get sort-columns sort-by [:c.created_at])
|
||||
order-by (conj (mapv #(vector % sort-direction) sort-exprs)
|
||||
@@ -120,29 +132,34 @@
|
||||
:offset offset)
|
||||
where (assoc :where where)))))
|
||||
|
||||
(defn conversation-count
|
||||
(mu/defn conversation-count
|
||||
"The total count of conversations matching the same criteria as [[list-conversations]] (ignoring sort, offset,
|
||||
and limit)."
|
||||
[{:keys [user-id group-id tenant-id date]}]
|
||||
[{:keys [user-id group-id tenant-id date]}
|
||||
:- [:map {:closed true}
|
||||
[:user-id {:optional true} [:maybe ::lib.schema.id/user]]
|
||||
[:group-id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:tenant-id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:date {:optional true} [:maybe :string]]]]
|
||||
(let [where (conversation-list-expr {:user-id user-id :group-id group-id :tenant-id tenant-id :date date})]
|
||||
(:count (t2/query-one (cond-> {:select [[[:count :*] :count]]
|
||||
:from [[:metabot_conversation :c]]}
|
||||
where (assoc :where where))))))
|
||||
|
||||
(defn messages-for-conversation
|
||||
(mu/defn messages-for-conversation
|
||||
"The MetabotMessages of the MetabotConversation with `conversation-id`, oldest first."
|
||||
[conversation-id]
|
||||
[conversation-id :- :string]
|
||||
(t2/select :model/MetabotMessage :conversation_id conversation-id {:order-by [[:created_at :asc] [:id :asc]]}))
|
||||
|
||||
(defn message-data-for-conversations
|
||||
(mu/defn message-data-for-conversations
|
||||
"The conversation, data, and data version of the MetabotMessages of the MetabotConversations with
|
||||
`conversation-ids`."
|
||||
[conversation-ids]
|
||||
[conversation-ids :- [:sequential :string]]
|
||||
(t2/select [:model/MetabotMessage :conversation_id :data :data_version] :conversation_id [:in conversation-ids]))
|
||||
|
||||
(defn feedback-for-conversation
|
||||
(mu/defn feedback-for-conversation
|
||||
"The MetabotFeedback rows on the messages of the MetabotConversation with `conversation-id`, oldest first."
|
||||
[conversation-id]
|
||||
[conversation-id :- :string]
|
||||
(t2/select :model/MetabotFeedback
|
||||
{:select [:metabot_feedback.id
|
||||
:metabot_feedback.message_id
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for transactions."
|
||||
(:require
|
||||
[clojure.string :as str]
|
||||
[malli.util :as mut]
|
||||
[metabase.auth-identity.schema :as auth-identity.schema]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.honey-sql-2 :as h2x]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(def ^:private totp-provider "totp")
|
||||
@@ -53,65 +58,80 @@
|
||||
[:like :%lower.last_name pattern]
|
||||
[:like :%lower.email pattern]]))))
|
||||
|
||||
(defn user
|
||||
(mu/defn user
|
||||
"The User with `user-id`, or nil."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one :model/User :id user-id))
|
||||
|
||||
(defn user-email
|
||||
(mu/defn user-email
|
||||
"The email of the User with `user-id`."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one-fn :email :model/User :id user-id))
|
||||
|
||||
(defn lock-user
|
||||
(mu/defn lock-user
|
||||
"The `:id` row of the User with `user-id`, locked for update."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one [:model/User :id] :id user-id {:for :update}))
|
||||
|
||||
(defn totp-identity
|
||||
(mu/defn totp-identity
|
||||
"The TOTP AuthIdentity of the User with `user-id`, or nil."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one :model/AuthIdentity :user_id user-id :provider totp-provider))
|
||||
|
||||
(defn lock-totp-identity
|
||||
(mu/defn lock-totp-identity
|
||||
"The TOTP AuthIdentity of the User with `user-id`, locked for update, or nil."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one :model/AuthIdentity :user_id user-id :provider totp-provider {:for :update}))
|
||||
|
||||
(defn password-credentials
|
||||
(mu/defn password-credentials
|
||||
"The password credentials of the User with `user-id`, or nil."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one-fn :credentials :model/AuthIdentity :user_id user-id :provider "password"))
|
||||
|
||||
(defn insert-auth-identity!
|
||||
"Insert the AuthIdentity `row`."
|
||||
[row]
|
||||
(mu/defn insert-auth-identity!
|
||||
"Insert the AuthIdentity `row`, returning the number inserted."
|
||||
[row :- [:map {:closed true}
|
||||
[:id {:optional true} ms/PositiveInt]
|
||||
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
|
||||
[:provider {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:credentials {:optional true} [:maybe :map]]
|
||||
[:metadata {:optional true} [:maybe :map]]
|
||||
[:provider_id {:optional true} [:maybe :string]]
|
||||
[:last_used_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:expires_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:confirmed_at {:optional true} [:maybe ms/TemporalInstant]]]]
|
||||
(t2/insert! :model/AuthIdentity row))
|
||||
|
||||
(defn update-auth-identity!
|
||||
"Apply `changes` to the AuthIdentity with `auth-identity-id`."
|
||||
[auth-identity-id changes]
|
||||
(mu/defn update-auth-identity!
|
||||
"Apply `changes` to the AuthIdentity with `auth-identity-id`, returning the number updated."
|
||||
[auth-identity-id :- ms/PositiveInt
|
||||
changes :- (mut/select-keys ::auth-identity.schema/auth-identity.update [:credentials :confirmed_at])]
|
||||
(t2/update! :model/AuthIdentity auth-identity-id changes))
|
||||
|
||||
(defn delete-totp-identity!
|
||||
(mu/defn delete-totp-identity!
|
||||
"Delete the TOTP AuthIdentity of the User with `user-id`, returning the number deleted."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/delete! :model/AuthIdentity :user_id user-id :provider totp-provider))
|
||||
|
||||
(defn confirmed-totp-count
|
||||
(mu/defn confirmed-totp-count
|
||||
"The number of confirmed TOTP enrollments."
|
||||
[]
|
||||
(t2/count :model/AuthIdentity :provider totp-provider :confirmed_at [:not= nil]))
|
||||
|
||||
(defn unenrolled-user-count
|
||||
(mu/defn unenrolled-user-count
|
||||
"The number of active personal Users without a confirmed TOTP enrollment."
|
||||
[]
|
||||
(t2/count :model/User {:where unenrolled-user-where}))
|
||||
|
||||
(defn user-list
|
||||
(mu/defn user-list
|
||||
"The name-ordered admin list of enrolled (with their enrollment time) or unenrolled Users matching `search`, paged
|
||||
by the optional `limit` and `offset`."
|
||||
[enrolled? search limit offset]
|
||||
[enrolled? :- :boolean
|
||||
search :- [:maybe :string]
|
||||
limit :- [:maybe ms/PositiveInt]
|
||||
offset :- [:maybe ms/IntGreaterThanOrEqualToZero]]
|
||||
(t2/select :model/User
|
||||
(cond-> {:select (cond-> list-columns enrolled? (into enrolled-at-select))
|
||||
:where (user-list-where enrolled? search)
|
||||
@@ -121,7 +141,8 @@
|
||||
;; a nil limit would emit `LIMIT NULL`
|
||||
limit (assoc :limit limit :offset offset))))
|
||||
|
||||
(defn user-list-count
|
||||
(mu/defn user-list-count
|
||||
"The number of enrolled or unenrolled Users matching `search`."
|
||||
[enrolled? search]
|
||||
[enrolled? :- :boolean
|
||||
search :- [:maybe :string]]
|
||||
(t2/count :model/User {:where (user-list-where enrolled? search)}))
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
"Application database queries for the permission-debug module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.permissions.core :as perms]
|
||||
[metabase.util.malli :as mu]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn user-superuser?
|
||||
(mu/defn user-superuser?
|
||||
"Whether the User with `user-id` is a superuser."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one-fn :is_superuser :model/User :id user-id))
|
||||
|
||||
(defn card
|
||||
(mu/defn card
|
||||
"The Card with `card-id`, or nil."
|
||||
[card-id]
|
||||
[card-id :- ::lib.schema.id/card]
|
||||
(t2/select-one :model/Card :id card-id))
|
||||
|
||||
(defn- blocked-tables-select
|
||||
@@ -31,14 +33,26 @@
|
||||
{:user-id user-id :is-superuser? false}
|
||||
permissions-granting)]]]})
|
||||
|
||||
(defn blocked-tables-in-database
|
||||
(mu/defn blocked-tables-in-database
|
||||
"The `[db-name schema table-name group-name]` rows of every Table in the Database with `database-id` blocked (per
|
||||
`permissions-blocking`) for the User with `user-id`, excluding those granted (per `permissions-granting`)."
|
||||
[user-id database-id permissions-blocking permissions-granting]
|
||||
`permissions-blocking`) for the User with `user-id`, excluding those granted (per `permissions-granting`).
|
||||
|
||||
`permissions-blocking`/`permissions-granting` are plain permission-type -> permission-value (or
|
||||
`[value :most|:least]`) mappings, per `perms/PermissionMapping`; db.clj builds the Honey SQL from them."
|
||||
[user-id :- ::lib.schema.id/user
|
||||
database-id :- ::lib.schema.id/database
|
||||
permissions-blocking :- perms/PermissionMapping
|
||||
permissions-granting :- perms/PermissionMapping]
|
||||
(t2/query (blocked-tables-select user-id [:= :blocked.db_id database-id] permissions-blocking permissions-granting)))
|
||||
|
||||
(defn blocked-tables-among
|
||||
(mu/defn blocked-tables-among
|
||||
"The `[db-name schema table-name group-name]` rows of the Tables with `table-ids` blocked (per
|
||||
`permissions-blocking`) for the User with `user-id`, excluding those granted (per `permissions-granting`)."
|
||||
[user-id table-ids permissions-blocking permissions-granting]
|
||||
`permissions-blocking`) for the User with `user-id`, excluding those granted (per `permissions-granting`).
|
||||
|
||||
`permissions-blocking`/`permissions-granting` are plain permission-type -> permission-value (or
|
||||
`[value :most|:least]`) mappings, per `perms/PermissionMapping`; db.clj builds the Honey SQL from them."
|
||||
[user-id :- ::lib.schema.id/user
|
||||
table-ids :- [:set ::lib.schema.id/table]
|
||||
permissions-blocking :- perms/PermissionMapping
|
||||
permissions-granting :- perms/PermissionMapping]
|
||||
(t2/query (blocked-tables-select user-id [:in :blocked.id table-ids] permissions-blocking permissions-granting)))
|
||||
|
||||
@@ -2,25 +2,62 @@
|
||||
"Application database queries for the remote-sync module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so no other namespace in the module runs a query itself."
|
||||
(:require
|
||||
[metabase-enterprise.remote-sync.schema :as remote-sync.schema]
|
||||
[metabase.collections.core :as collections]
|
||||
[metabase.collections.schema :as collections.schema]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn eligible-children
|
||||
(def ^:private Conditions
|
||||
"A map of column to value (possibly nil) or Toucan 2 operator-vector value, or nil for none."
|
||||
[:maybe [:map-of :keyword [:maybe [:or :string :int :boolean :keyword sequential?]]]])
|
||||
|
||||
(def ^:private RemovalOpts
|
||||
"The `:scope-key`, `:synced-collection-ids`, `:entity-ids`, and `:removal-conditions` describing which rows an
|
||||
import removes (see [[removal-exprs]])."
|
||||
[:map {:closed true}
|
||||
[:scope-key [:maybe :keyword]]
|
||||
[:synced-collection-ids [:maybe [:or [:set ::lib.schema.id/collection] [:sequential ::lib.schema.id/collection]]]]
|
||||
[:entity-ids [:maybe [:or [:set :string] [:sequential :string]]]]
|
||||
[:removal-conditions Conditions]])
|
||||
|
||||
(def ^:private ModelId
|
||||
"A `:model_id`: the primary key of the referenced entity, or the `-1` sentinel
|
||||
(`metabase-enterprise.remote-sync.settings/transforms-root-id`) standing in for the virtual Transforms root
|
||||
Collection."
|
||||
[:or ms/PositiveInt [:= -1]])
|
||||
|
||||
(def ^:private Path
|
||||
"A `{:db_name :schema :table_name :field_name}` path used to locate a Table or Field."
|
||||
[:map
|
||||
[:db_name :string]
|
||||
[:schema {:optional true} [:maybe :string]]
|
||||
[:table_name :string]
|
||||
[:field_name {:optional true} :string]])
|
||||
|
||||
(mu/defn eligible-children
|
||||
"The instances of `model-key` whose `fk` column equals `model-id`, additionally matching `conditions` (a map of
|
||||
column to value or Toucan 2 operator-vector value, or nil for none)."
|
||||
[model-key fk model-id conditions]
|
||||
[model-key :- :keyword
|
||||
fk :- :keyword
|
||||
model-id :- ms/PositiveInt
|
||||
conditions :- Conditions]
|
||||
(apply t2/select model-key fk model-id (mapcat identity conditions)))
|
||||
|
||||
(defn ids-where
|
||||
(mu/defn ids-where
|
||||
"The IDs of the instances of `model-key` matching `conditions` (a map of column to value or Toucan 2
|
||||
operator-vector value, or nil for every instance)."
|
||||
[model-key conditions]
|
||||
[model-key :- :keyword
|
||||
conditions :- Conditions]
|
||||
(apply t2/select-fn-set :id model-key (mapcat identity conditions)))
|
||||
|
||||
(defn count-where
|
||||
(mu/defn count-where
|
||||
"The number of instances of `model-key` matching `conditions` (a map of column to value or Toucan 2
|
||||
operator-vector value, or nil for every instance)."
|
||||
[model-key conditions]
|
||||
[model-key :- :keyword
|
||||
conditions :- Conditions]
|
||||
(apply t2/count model-key (mapcat identity conditions)))
|
||||
|
||||
(defn- removal-condition-exprs
|
||||
@@ -45,10 +82,11 @@
|
||||
(seq entity-ids) (conj [:not-in :entity_id entity-ids])
|
||||
:always (into (removal-condition-exprs removal-conditions)))))
|
||||
|
||||
(defn delete-removed-instances!
|
||||
(mu/defn delete-removed-instances!
|
||||
"Deletes the `model-key` rows an import removes (see [[removal-exprs]]); a no-op for a scoped model with no
|
||||
synced collections, and a delete of every row when nothing restricts it."
|
||||
[model-key removal-opts]
|
||||
[model-key :- :keyword
|
||||
removal-opts :- RemovalOpts]
|
||||
(when-let [exprs (removal-exprs removal-opts)]
|
||||
(if (seq exprs)
|
||||
(t2/delete! model-key {:where (if (= 1 (count exprs)) (first exprs) (into [:and] exprs))})
|
||||
@@ -71,50 +109,64 @@
|
||||
(let [id-column (keyword (str (name (t2/table-name model-key)) ".id"))]
|
||||
(into [:and (unsynced-anti-join-expr model-type id-column)] (removal-exprs removal-opts))))
|
||||
|
||||
(defn unsynced-instance-count
|
||||
(mu/defn unsynced-instance-count
|
||||
"The number of `model-key` rows [[delete-removed-instances!]] would remove (see [[removal-exprs]]) that also
|
||||
have no RemoteSyncObject of `model-type` in synced status — unsynced local work an import would otherwise wipe
|
||||
out."
|
||||
[model-key model-type removal-opts]
|
||||
[model-key :- :keyword
|
||||
model-type :- :string
|
||||
removal-opts :- RemovalOpts]
|
||||
(t2/count model-key {:where (unsynced-instance-expr model-key model-type removal-opts)}))
|
||||
|
||||
(defn unsynced-instance-names
|
||||
(mu/defn unsynced-instance-names
|
||||
"Up to `limit` names of the rows [[unsynced-instance-count]] counts."
|
||||
[model-key model-type removal-opts limit]
|
||||
[model-key :- :keyword
|
||||
model-type :- :string
|
||||
removal-opts :- RemovalOpts
|
||||
limit :- ms/PositiveInt]
|
||||
(t2/select-fn-vec :name model-key {:where (unsynced-instance-expr model-key model-type removal-opts)
|
||||
:limit limit}))
|
||||
|
||||
(defn instance
|
||||
(mu/defn instance
|
||||
"The instance of `model` with `id`, or nil."
|
||||
[model id]
|
||||
[model :- :keyword
|
||||
id :- ms/PositiveInt]
|
||||
(t2/select-one model :id id))
|
||||
|
||||
(defn instance-with-columns
|
||||
(mu/defn instance-with-columns
|
||||
"The `columns` of the instance of `model` with `id`, or nil."
|
||||
[model columns id]
|
||||
[model :- :keyword
|
||||
columns :- [:sequential :keyword]
|
||||
id :- ms/PositiveInt]
|
||||
(t2/select-one (into [model] columns) :id id))
|
||||
|
||||
(defn instance-names
|
||||
(mu/defn instance-names
|
||||
"The `:id` and `:name` of the instances of `model` with `ids`."
|
||||
[model ids]
|
||||
[model :- :keyword
|
||||
ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select [model :id :name] :id [:in ids]))
|
||||
|
||||
(defn instances-in-collections
|
||||
(mu/defn instances-in-collections
|
||||
"The instances of `model` in the Collections with `collection-ids`, excluding those archived under the optional
|
||||
`archived-key` column."
|
||||
[model collection-ids archived-key]
|
||||
[model :- :keyword
|
||||
collection-ids :- [:sequential ::lib.schema.id/collection]
|
||||
archived-key :- [:maybe :keyword]]
|
||||
(t2/select model {:where [:and
|
||||
[:in :collection_id collection-ids]
|
||||
(when archived-key [:= archived-key false])]}))
|
||||
|
||||
(defn instances-with-columns-by-entity-ids
|
||||
(mu/defn instances-with-columns-by-entity-ids
|
||||
"The `columns` of the instances of `model` with `entity-ids`."
|
||||
[model columns entity-ids]
|
||||
[model :- :keyword
|
||||
columns :- [:sequential :keyword]
|
||||
entity-ids :- [:set :string]]
|
||||
(t2/select (into [model] columns) :entity_id [:in entity-ids]))
|
||||
|
||||
(defn delete-instances!
|
||||
(mu/defn delete-instances!
|
||||
"Delete the instances of `model` with `ids`."
|
||||
[model ids]
|
||||
[model :- :keyword
|
||||
ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/delete! model :id [:in ids]))
|
||||
|
||||
(defn- tracking-select-parts
|
||||
@@ -136,17 +188,19 @@
|
||||
:from [[:measure :s]]
|
||||
:join [[:metabase_table :t] [:= :s.table_id :t.id]]}))
|
||||
|
||||
(defn tracking-details-by-id
|
||||
(mu/defn tracking-details-by-id
|
||||
"The name, table id, collection id, and table name of the `model-key` (Field, Segment, or Measure) instance with
|
||||
`model-id`, or nil."
|
||||
[model-key model-id]
|
||||
[model-key :- :keyword
|
||||
model-id :- ms/PositiveInt]
|
||||
(let [{:keys [alias select from join]} (tracking-select-parts model-key)]
|
||||
(first (t2/query {:select select :from from :join join :where [:= (keyword alias "id") model-id]}))))
|
||||
|
||||
(defn tracking-details-by-entity-ids
|
||||
(mu/defn tracking-details-by-entity-ids
|
||||
"The `:id`, name, table id, collection id, and table name of the `model-key` (Segment or Measure) instances with
|
||||
`entity-ids`."
|
||||
[model-key entity-ids]
|
||||
[model-key :- :keyword
|
||||
entity-ids :- [:or [:set :string] [:sequential :string]]]
|
||||
(let [{:keys [alias select from join]} (tracking-select-parts model-key)
|
||||
id-column (keyword alias "id")]
|
||||
(t2/query {:select (into [id-column] select)
|
||||
@@ -154,24 +208,28 @@
|
||||
:join join
|
||||
:where [:in (keyword alias "entity_id") entity-ids]})))
|
||||
|
||||
(defn entity-id
|
||||
(mu/defn entity-id
|
||||
"The entity ID of the instance of `model` with `id`."
|
||||
[model id]
|
||||
[model :- :keyword
|
||||
id :- ms/PositiveInt]
|
||||
(t2/select-one-fn :entity_id model :id id))
|
||||
|
||||
(defn entity-ids-by-id
|
||||
(mu/defn entity-ids-by-id
|
||||
"A map of ID to entity ID for the instances of `model` with `ids`."
|
||||
[model ids]
|
||||
[model :- :keyword
|
||||
ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select-pk->fn :entity_id model :id [:in ids]))
|
||||
|
||||
(defn existing-entity-ids
|
||||
(mu/defn existing-entity-ids
|
||||
"The subset of `entity-ids` that instances of `model` have."
|
||||
[model entity-ids]
|
||||
[model :- :keyword
|
||||
entity-ids :- [:or [:set :string] [:sequential :string]]]
|
||||
(t2/select-fn-set :entity_id model :entity_id [:in entity-ids]))
|
||||
|
||||
(defn ids-by-entity-ids
|
||||
(mu/defn ids-by-entity-ids
|
||||
"The IDs of the instances of `model` with `entity-ids`."
|
||||
[model entity-ids]
|
||||
[model :- :keyword
|
||||
entity-ids :- [:set :string]]
|
||||
(t2/select-pks-vec model :entity_id [:in entity-ids]))
|
||||
|
||||
(defn- path-expr
|
||||
@@ -188,35 +246,35 @@
|
||||
(and has-field? field_name)
|
||||
(conj [:= :f.name field_name]))))))
|
||||
|
||||
(defn tables-at-paths
|
||||
(mu/defn tables-at-paths
|
||||
"The `:id`, `:name`, and `:collection_id` rows of the Tables at `paths` (`{:db_name :schema :table_name}`)."
|
||||
[paths]
|
||||
[paths :- [:sequential Path]]
|
||||
(t2/query {:select [:t.id :t.name :t.collection_id]
|
||||
:from [[:metabase_table :t]]
|
||||
:join [[:metabase_database :db] [:= :db.id :t.db_id]]
|
||||
:where (path-expr paths false)}))
|
||||
|
||||
(defn fields-at-paths
|
||||
(mu/defn fields-at-paths
|
||||
"The `:id`, `:name`, `:table_id`, `:collection_id`, and `:table_name` rows of the Fields at `paths`
|
||||
(`{:db_name :schema :table_name :field_name}`)."
|
||||
[paths]
|
||||
[paths :- [:sequential Path]]
|
||||
(t2/query {:select [:f.id :f.name :f.table_id [:t.collection_id :collection_id] [:t.name :table_name]]
|
||||
:from [[:metabase_field :f]]
|
||||
:join [[:metabase_table :t] [:= :t.id :f.table_id]
|
||||
[:metabase_database :db] [:= :db.id :t.db_id]]
|
||||
:where (path-expr paths true)}))
|
||||
|
||||
(defn card-types
|
||||
(mu/defn card-types
|
||||
"The `:id`, `:type`, and `:card_schema` of the Cards with `card-ids`."
|
||||
[card-ids]
|
||||
[card-ids :- [:sequential ::lib.schema.id/card]]
|
||||
(t2/select [:model/Card :id :type :card_schema] :id [:in card-ids]))
|
||||
|
||||
(defn field-user-settings-exist?
|
||||
(mu/defn field-user-settings-exist?
|
||||
"Whether the Field with `field-id` has FieldUserSettings."
|
||||
[field-id]
|
||||
[field-id :- ::lib.schema.id/field]
|
||||
(t2/exists? :model/FieldUserSettings :field_id field-id))
|
||||
|
||||
(defn snippets
|
||||
(mu/defn snippets
|
||||
"The `:id`, `:name`, and `:collection_id` of every NativeQuerySnippet."
|
||||
[]
|
||||
(t2/select [:model/NativeQuerySnippet :id :name :collection_id]))
|
||||
@@ -228,57 +286,57 @@
|
||||
(for [collection collections]
|
||||
[:like :location (str (collections/location-path collection) "%")])))
|
||||
|
||||
(defn collections
|
||||
(mu/defn collections
|
||||
"The Collections with `collection-ids`."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:sequential ::lib.schema.id/collection]]
|
||||
(t2/select :model/Collection :id [:in collection-ids]))
|
||||
|
||||
(defn collections-by-id
|
||||
(mu/defn collections-by-id
|
||||
"A map of ID to the ID, name, location, and personal owner of the Collections with `collection-ids`."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:set ::lib.schema.id/collection]]
|
||||
(t2/select-pk->fn identity [:model/Collection :id :name :location :personal_owner_id] :id [:in collection-ids]))
|
||||
|
||||
(defn collection-sync-states
|
||||
(mu/defn collection-sync-states
|
||||
"The `:id` and `:is_remote_synced` of the Collections with `collection-ids`."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:sequential ::lib.schema.id/collection]]
|
||||
(t2/select [:model/Collection :id :is_remote_synced] :id [:in collection-ids]))
|
||||
|
||||
(defn collection-name-and-id
|
||||
(mu/defn collection-name-and-id
|
||||
"The `:name` and `:collection_id` (its own ID) of the Collection with `collection-id`, or nil."
|
||||
[collection-id]
|
||||
[collection-id :- ::lib.schema.id/collection]
|
||||
(t2/select-one [:model/Collection :name [:id :collection_id]] :id collection-id))
|
||||
|
||||
(defn library-collection
|
||||
(mu/defn library-collection
|
||||
"The Library Collection of `library-type`, or nil."
|
||||
[library-type]
|
||||
[library-type :- :string]
|
||||
(t2/select-one :model/Collection :type library-type))
|
||||
|
||||
(defn snippet-collections
|
||||
(mu/defn snippet-collections
|
||||
"The `:id` and `:name` of the Collections of the snippets namespace."
|
||||
[]
|
||||
(t2/select [:model/Collection :id :name] :namespace "snippets"))
|
||||
|
||||
(defn snippet-collection-ids
|
||||
(mu/defn snippet-collection-ids
|
||||
"The IDs of the Collections of the snippets namespace."
|
||||
[]
|
||||
(t2/select-pks-set :model/Collection :namespace "snippets"))
|
||||
|
||||
(defn collections-in-namespace
|
||||
(mu/defn collections-in-namespace
|
||||
"The `:id` and `:entity_id` of the Collections of `namespace-name`."
|
||||
[namespace-name]
|
||||
[namespace-name :- :string]
|
||||
(t2/select [:model/Collection :id :entity_id] :namespace namespace-name))
|
||||
|
||||
(defn collection-ids-in-namespace
|
||||
(mu/defn collection-ids-in-namespace
|
||||
"The IDs of the Collections of `namespace-name`."
|
||||
[namespace-name]
|
||||
[namespace-name :- :string]
|
||||
(t2/select-pks-vec :model/Collection :namespace namespace-name))
|
||||
|
||||
(defn remote-synced-collection-ids
|
||||
(mu/defn remote-synced-collection-ids
|
||||
"The IDs of the remote-synced Collections."
|
||||
[]
|
||||
(t2/select-pks-vec :model/Collection :is_remote_synced true))
|
||||
|
||||
(defn unarchived-remote-synced-root-collection-ids
|
||||
(mu/defn unarchived-remote-synced-root-collection-ids
|
||||
"The IDs of the unarchived remote-synced root Collections."
|
||||
[]
|
||||
(t2/select-fn-set :id :model/Collection
|
||||
@@ -287,40 +345,40 @@
|
||||
[:= :location "/"]
|
||||
[:not :archived]]}))
|
||||
|
||||
(defn unarchived-root-collection-ids-in-namespace
|
||||
(mu/defn unarchived-root-collection-ids-in-namespace
|
||||
"The IDs of the unarchived root Collections of `namespace-name`."
|
||||
[namespace-name]
|
||||
[namespace-name :- :string]
|
||||
(t2/select-fn-set :id :model/Collection
|
||||
{:where [:and
|
||||
[:= :namespace namespace-name]
|
||||
[:= :location "/"]
|
||||
[:not :archived]]}))
|
||||
|
||||
(defn subtree-collection-ids
|
||||
(mu/defn subtree-collection-ids
|
||||
"The IDs of `collections` and all of their descendants."
|
||||
[collections]
|
||||
[collections :- [:sequential ::collections.schema/collection]]
|
||||
(t2/select-pks-set :model/Collection {:where (subtree-expr collections)}))
|
||||
|
||||
(defn remote-synced-subtree-collection-ids
|
||||
(mu/defn remote-synced-subtree-collection-ids
|
||||
"The IDs of the remote-synced Collections among `collections` and their descendants."
|
||||
[collections]
|
||||
[collections :- [:sequential ::collections.schema/collection]]
|
||||
(t2/select-pks-set :model/Collection
|
||||
{:where [:and
|
||||
[:= :is_remote_synced true]
|
||||
(subtree-expr collections)]}))
|
||||
|
||||
(defn mark-subtree-remote-synced!
|
||||
(mu/defn mark-subtree-remote-synced!
|
||||
"Mark `collections` and all of their descendants as remote-synced."
|
||||
[collections]
|
||||
[collections :- [:sequential ::collections.schema/collection]]
|
||||
(t2/query {:update (t2/table-name :model/Collection)
|
||||
:set {:is_remote_synced true}
|
||||
:where [:and
|
||||
[:= :is_remote_synced false]
|
||||
(subtree-expr collections)]}))
|
||||
|
||||
(defn unmark-collections-remote-synced!
|
||||
(mu/defn unmark-collections-remote-synced!
|
||||
"Mark the Collections with `collection-ids` as not remote-synced."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:set ::lib.schema.id/collection]]
|
||||
(t2/query {:update (t2/table-name :model/Collection)
|
||||
:set {:is_remote_synced false}
|
||||
:where [:in :id collection-ids]}))
|
||||
@@ -339,137 +397,151 @@
|
||||
[:and [:= :model_type model_type] [:= :model_id model_id]]))
|
||||
rows))
|
||||
|
||||
(defn rso
|
||||
(mu/defn rso
|
||||
"The RemoteSyncObject of the entity `model-type` `model-id`, or nil."
|
||||
[model-type model-id]
|
||||
[model-type :- :string
|
||||
model-id :- ModelId]
|
||||
(t2/select-one :model/RemoteSyncObject :model_type model-type :model_id model-id))
|
||||
|
||||
(defn lock-rso
|
||||
(mu/defn lock-rso
|
||||
"The RemoteSyncObject of the entity `model-type` `model-id`, locked for update, or nil."
|
||||
[model-type model-id]
|
||||
[model-type :- :string
|
||||
model-id :- ModelId]
|
||||
(t2/select-one :model/RemoteSyncObject
|
||||
{:where [:and [:= :model_type model-type] [:= :model_id model-id]]
|
||||
:for :update}))
|
||||
|
||||
(defn rso-by-file-path
|
||||
(mu/defn rso-by-file-path
|
||||
"The RemoteSyncObject at `file-path`, or nil."
|
||||
[file-path]
|
||||
[file-path :- :string]
|
||||
(t2/select-one :model/RemoteSyncObject :file_path file-path))
|
||||
|
||||
(defn rso-exists?
|
||||
(mu/defn rso-exists?
|
||||
"Whether the entity `model-type` `model-id` has a RemoteSyncObject."
|
||||
[model-type model-id]
|
||||
[model-type :- :string
|
||||
model-id :- ModelId]
|
||||
(t2/exists? :model/RemoteSyncObject :model_type model-type :model_id model-id))
|
||||
|
||||
(defn rso-of-type-exists?
|
||||
(mu/defn rso-of-type-exists?
|
||||
"Whether any entity of `model-type` has a RemoteSyncObject."
|
||||
[model-type]
|
||||
[model-type :- :string]
|
||||
(t2/exists? :model/RemoteSyncObject :model_type model-type))
|
||||
|
||||
(defn rso-count-of-type
|
||||
(mu/defn rso-count-of-type
|
||||
"The number of RemoteSyncObjects of `model-type`."
|
||||
[model-type]
|
||||
[model-type :- :string]
|
||||
(t2/count :model/RemoteSyncObject :model_type model-type))
|
||||
|
||||
(defn rso-keys
|
||||
(mu/defn rso-keys
|
||||
"The `:id`, `:model_type`, and `:model_id` of every RemoteSyncObject."
|
||||
[]
|
||||
(t2/select [:model/RemoteSyncObject :id :model_type :model_id]))
|
||||
|
||||
(defn departed-rso-keys
|
||||
(mu/defn departed-rso-keys
|
||||
"The `:id`, `:model_type`, and `:model_id` of the RemoteSyncObjects pending removal or deletion."
|
||||
[]
|
||||
(t2/select [:model/RemoteSyncObject :id :model_type :model_id] :status [:in ["removed" "delete"]]))
|
||||
|
||||
(defn all-rso-ids
|
||||
(mu/defn all-rso-ids
|
||||
"The IDs of every RemoteSyncObject."
|
||||
[]
|
||||
(t2/select-pks-set :model/RemoteSyncObject))
|
||||
|
||||
(defn unsynced-rsos
|
||||
(mu/defn unsynced-rsos
|
||||
"The RemoteSyncObjects whose status is not synced."
|
||||
[]
|
||||
(t2/select :model/RemoteSyncObject {:where [:not= :status "synced"]}))
|
||||
|
||||
(defn dirty-rso-exists?
|
||||
(mu/defn dirty-rso-exists?
|
||||
"Whether a RemoteSyncObject of a model type other than `excluded-model-types` is not synced."
|
||||
[excluded-model-types]
|
||||
[excluded-model-types :- [:set :string]]
|
||||
(t2/exists? :model/RemoteSyncObject
|
||||
{:where [:and
|
||||
[:not= :status "synced"]
|
||||
(when (seq excluded-model-types)
|
||||
[:not-in :model_type excluded-model-types])]}))
|
||||
|
||||
(defn dirty-rsos
|
||||
(mu/defn dirty-rsos
|
||||
"The RemoteSyncObjects of model types other than `excluded-model-types` that are not synced."
|
||||
[excluded-model-types]
|
||||
[excluded-model-types :- [:set :string]]
|
||||
(t2/select :model/RemoteSyncObject
|
||||
{:where [:and
|
||||
[:not= :status "synced"]
|
||||
(when (seq excluded-model-types)
|
||||
[:not-in :model_type excluded-model-types])]}))
|
||||
|
||||
(defn tracked-model-ids
|
||||
(mu/defn tracked-model-ids
|
||||
"The model IDs of the RemoteSyncObjects of `model-type`."
|
||||
[model-type]
|
||||
[model-type :- :string]
|
||||
(t2/select-fn-set :model_id :model/RemoteSyncObject :model_type model-type))
|
||||
|
||||
(defn rsos-of-models
|
||||
(mu/defn rsos-of-models
|
||||
"The RemoteSyncObjects of the entities of `model-type` with `model-ids`."
|
||||
[model-type model-ids]
|
||||
[model-type :- :string
|
||||
model-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select :model/RemoteSyncObject :model_type model-type :model_id [:in model-ids]))
|
||||
|
||||
(defn active-child-rsos
|
||||
(mu/defn active-child-rsos
|
||||
"The RemoteSyncObjects of `model-type` under the Table with `table-id` that are not pending removal or deletion."
|
||||
[model-type table-id]
|
||||
[model-type :- :string
|
||||
table-id :- ::lib.schema.id/table]
|
||||
(t2/select :model/RemoteSyncObject
|
||||
:model_type model-type
|
||||
:model_table_id table-id
|
||||
:status [:not-in ["removed" "delete"]]))
|
||||
|
||||
(defn content-rso-statuses
|
||||
(mu/defn content-rso-statuses
|
||||
"The `:id` and `:status` of the RemoteSyncObjects of the Collections with `collection-ids` and their contents."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:set ::lib.schema.id/collection]]
|
||||
(t2/select [:model/RemoteSyncObject :id :status] {:where (contents-rso-expr collection-ids)}))
|
||||
|
||||
(defn removed-content-rso-ids
|
||||
(mu/defn removed-content-rso-ids
|
||||
"The IDs of the RemoteSyncObjects pending removal among those of the Collections with `collection-ids` and their
|
||||
contents."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:sequential ::lib.schema.id/collection]]
|
||||
(t2/select-pks-set :model/RemoteSyncObject
|
||||
{:where [:and
|
||||
[:= :status "removed"]
|
||||
(contents-rso-expr collection-ids)]}))
|
||||
|
||||
(defn insert-rso!
|
||||
(mu/defn insert-rso!
|
||||
"Insert the RemoteSyncObject `row`."
|
||||
[row]
|
||||
[row :- ::remote-sync.schema/remote-sync-object.update]
|
||||
(t2/insert! :model/RemoteSyncObject row))
|
||||
|
||||
(defn insert-rsos!
|
||||
(mu/defn insert-rsos!
|
||||
"Insert the RemoteSyncObject `rows`."
|
||||
[rows]
|
||||
[rows :- [:sequential ::remote-sync.schema/remote-sync-object.update]]
|
||||
(t2/insert! :model/RemoteSyncObject rows))
|
||||
|
||||
(defn update-rso!
|
||||
(mu/defn update-rso!
|
||||
"Apply `changes` to the RemoteSyncObject with `rso-id`."
|
||||
[rso-id changes]
|
||||
[rso-id :- ms/PositiveInt
|
||||
changes :- ::remote-sync.schema/remote-sync-object.update]
|
||||
(t2/update! :model/RemoteSyncObject rso-id changes))
|
||||
|
||||
(defn set-rsos-status!
|
||||
(mu/defn set-rsos-status!
|
||||
"Set the status of the RemoteSyncObjects with `rso-ids` to `status` as of `timestamp`."
|
||||
[rso-ids status timestamp]
|
||||
[rso-ids :- [:sequential ms/PositiveInt]
|
||||
status :- :string
|
||||
timestamp :- ms/TemporalInstant]
|
||||
(t2/update! :model/RemoteSyncObject :id [:in rso-ids] {:status status :status_changed_at timestamp}))
|
||||
|
||||
(defn mark-all-rsos-synced!
|
||||
(mu/defn mark-all-rsos-synced!
|
||||
"Mark every RemoteSyncObject as synced as of `timestamp`."
|
||||
[timestamp]
|
||||
[timestamp :- ms/TemporalInstant]
|
||||
(t2/update! :model/RemoteSyncObject {:status "synced" :status_changed_at timestamp}))
|
||||
|
||||
(defn mark-rsos-synced!
|
||||
(mu/defn mark-rsos-synced!
|
||||
"Mark the RemoteSyncObjects with `rso-ids` as synced as of `timestamp`, writing the `:file_path` and
|
||||
`:content_hash` of those in `metadata-by-id` and keeping the existing values of the others."
|
||||
[rso-ids metadata-by-id timestamp]
|
||||
[rso-ids :- [:sequential ms/PositiveInt]
|
||||
metadata-by-id :- [:map-of ms/PositiveInt
|
||||
[:map {:closed true}
|
||||
[:id {:optional true} ms/PositiveInt]
|
||||
[:file_path {:optional true} [:maybe :string]]
|
||||
[:content_hash {:optional true} [:maybe :string]]]]
|
||||
timestamp :- ms/TemporalInstant]
|
||||
(t2/update! :model/RemoteSyncObject
|
||||
{:id [:in (vec rso-ids)]}
|
||||
(cond-> {:status "synced" :status_changed_at timestamp}
|
||||
@@ -487,59 +559,61 @@
|
||||
metadata-by-id)
|
||||
[:else :content_hash]))))))
|
||||
|
||||
(defn delete-rso!
|
||||
(mu/defn delete-rso!
|
||||
"Delete the RemoteSyncObject with `rso-id`."
|
||||
[rso-id]
|
||||
[rso-id :- ms/PositiveInt]
|
||||
(t2/delete! :model/RemoteSyncObject rso-id))
|
||||
|
||||
(defn delete-rsos!
|
||||
(mu/defn delete-rsos!
|
||||
"Delete the RemoteSyncObjects with `rso-ids`."
|
||||
[rso-ids]
|
||||
[rso-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/delete! :model/RemoteSyncObject :id [:in rso-ids]))
|
||||
|
||||
(defn delete-rso-of!
|
||||
(mu/defn delete-rso-of!
|
||||
"Delete the RemoteSyncObject of the entity `model-type` `model-id`."
|
||||
[model-type model-id]
|
||||
[model-type :- :string
|
||||
model-id :- ModelId]
|
||||
(t2/delete! :model/RemoteSyncObject :model_type model-type :model_id model-id))
|
||||
|
||||
(defn delete-rsos-of-type!
|
||||
(mu/defn delete-rsos-of-type!
|
||||
"Delete the RemoteSyncObjects of `model-type`."
|
||||
[model-type]
|
||||
[model-type :- :string]
|
||||
(t2/delete! :model/RemoteSyncObject :model_type model-type))
|
||||
|
||||
(defn delete-rsos-of-models!
|
||||
(mu/defn delete-rsos-of-models!
|
||||
"Delete the RemoteSyncObjects of the entities of `model-type` with `model-ids`."
|
||||
[model-type model-ids]
|
||||
[model-type :- :string
|
||||
model-ids :- [:set ms/PositiveInt]]
|
||||
(t2/delete! :model/RemoteSyncObject :model_type model-type :model_id [:in model-ids]))
|
||||
|
||||
(defn delete-rsos-of-keys!
|
||||
"Delete the RemoteSyncObjects of the `[{:model_type :model_id}]` `rows`."
|
||||
[rows]
|
||||
(mu/defn delete-rsos-of-keys!
|
||||
"Delete the RemoteSyncObjects keyed by the `:model_type`/`:model_id` of `rows` (other keys are ignored)."
|
||||
[rows :- [:sequential [:map [:model_type :string] [:model_id ms/PositiveInt]]]]
|
||||
(t2/delete! :model/RemoteSyncObject {:where (rso-keys-expr rows)}))
|
||||
|
||||
(defn delete-all-rsos!
|
||||
(mu/defn delete-all-rsos!
|
||||
"Delete every RemoteSyncObject."
|
||||
[]
|
||||
(t2/delete! :model/RemoteSyncObject))
|
||||
|
||||
(defn task
|
||||
(mu/defn task
|
||||
"The RemoteSyncTask with `task-id`, or nil."
|
||||
[task-id]
|
||||
[task-id :- ms/PositiveInt]
|
||||
(t2/select-one :model/RemoteSyncTask task-id))
|
||||
|
||||
(defn lock-task
|
||||
(mu/defn lock-task
|
||||
"The RemoteSyncTask with `task-id`, locked for update, or nil."
|
||||
[task-id]
|
||||
[task-id :- ms/PositiveInt]
|
||||
(t2/select-one :model/RemoteSyncTask :id task-id {:for :update}))
|
||||
|
||||
(defn task-cancelled?
|
||||
(mu/defn task-cancelled?
|
||||
"The cancelled flag of the RemoteSyncTask with `task-id`."
|
||||
[task-id]
|
||||
[task-id :- ms/PositiveInt]
|
||||
(t2/select-one-fn :cancelled :model/RemoteSyncTask :id task-id))
|
||||
|
||||
(defn current-task
|
||||
(mu/defn current-task
|
||||
"The newest started, unfinished RemoteSyncTask that reported progress after `progress-cutoff`, or nil."
|
||||
[progress-cutoff]
|
||||
[progress-cutoff :- ms/TemporalInstant]
|
||||
(t2/select-one :model/RemoteSyncTask
|
||||
{:where [:and
|
||||
[:<> :started_at nil]
|
||||
@@ -549,7 +623,7 @@
|
||||
:order-by [[:started_at :desc]
|
||||
[:id :desc]]}))
|
||||
|
||||
(defn most-recent-task
|
||||
(mu/defn most-recent-task
|
||||
"The newest started RemoteSyncTask, or nil."
|
||||
[]
|
||||
(t2/select-one :model/RemoteSyncTask
|
||||
@@ -559,7 +633,7 @@
|
||||
:order-by [[:started_at :desc]
|
||||
[:id :desc]]}))
|
||||
|
||||
(defn last-successful-task
|
||||
(mu/defn last-successful-task
|
||||
"The newest finished RemoteSyncTask that was neither cancelled nor failed and recorded a version, or nil."
|
||||
[]
|
||||
(t2/select-one :model/RemoteSyncTask
|
||||
@@ -572,34 +646,47 @@
|
||||
:order-by [[:started_at :desc]
|
||||
[:id :desc]]}))
|
||||
|
||||
(defn insert-task!
|
||||
(mu/defn insert-task!
|
||||
"Insert `task` and return the new instance."
|
||||
[task]
|
||||
[task :- ::remote-sync.schema/remote-sync-task.update]
|
||||
(t2/insert-returning-instance! :model/RemoteSyncTask task))
|
||||
|
||||
(defn update-task!
|
||||
(mu/defn update-task!
|
||||
"Apply `changes` to the RemoteSyncTask with `task-id`."
|
||||
[task-id changes]
|
||||
[task-id :- ms/PositiveInt
|
||||
changes :- ::remote-sync.schema/remote-sync-task.update]
|
||||
(t2/update! :model/RemoteSyncTask task-id changes))
|
||||
|
||||
(defn supersede-stale-tasks!
|
||||
"Cancel and end at `now` the started, unfinished RemoteSyncTasks that last reported progress before `cutoff`."
|
||||
[cutoff now]
|
||||
(mu/defn end-task!
|
||||
"Apply `changes` to the RemoteSyncTask with `task-id` and mark it ended now."
|
||||
[task-id :- ms/PositiveInt
|
||||
changes :- ::remote-sync.schema/remote-sync-task.update]
|
||||
(t2/update! :model/RemoteSyncTask task-id (assoc changes :ended_at :%now)))
|
||||
|
||||
(mu/defn report-task-progress!
|
||||
"Record `progress` for the RemoteSyncTask with `task-id`, stamping the progress report time."
|
||||
[task-id :- ms/PositiveInt
|
||||
progress :- number?]
|
||||
(t2/update! :model/RemoteSyncTask task-id {:progress progress, :last_progress_report_at :%now}))
|
||||
|
||||
(mu/defn supersede-stale-tasks!
|
||||
"Cancel and end now the started, unfinished RemoteSyncTasks that last reported progress before `cutoff`."
|
||||
[cutoff :- ms/TemporalInstant]
|
||||
(t2/query {:update (t2/table-name :model/RemoteSyncTask)
|
||||
:set {:cancelled true
|
||||
:ended_at now
|
||||
:ended_at :%now
|
||||
:error_message "Superseded after staleness timeout"}
|
||||
:where [:and
|
||||
[:<> :started_at nil]
|
||||
[:= :ended_at nil]
|
||||
[:< :last_progress_report_at cutoff]]}))
|
||||
|
||||
(defn delete-tasks-started-before!
|
||||
(mu/defn delete-tasks-started-before!
|
||||
"Delete the RemoteSyncTasks started before `cutoff`, returning the number deleted."
|
||||
[cutoff]
|
||||
[cutoff :- ms/TemporalInstant]
|
||||
(t2/delete! :model/RemoteSyncTask {:where [:< :started_at cutoff]}))
|
||||
|
||||
(defn users-by-id
|
||||
(mu/defn users-by-id
|
||||
"A map of User ID to User for `user-ids`."
|
||||
[user-ids]
|
||||
[user-ids :- [:sequential [:maybe ::lib.schema.id/user]]]
|
||||
(t2/select-pk->fn identity :model/User :id [:in user-ids]))
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
[task]
|
||||
(when-let [existing (current-task)]
|
||||
(throw (ex-info "A running task exists" {:existing-task existing})))
|
||||
task)
|
||||
(merge {:started_at (mi/now)} task))
|
||||
|
||||
;;; ------------------------------------------- Helper Functions -------------------------------------------
|
||||
|
||||
@@ -49,8 +49,7 @@
|
||||
[additional-fields :- [:map]]]
|
||||
(remote-sync.db/insert-task! (merge {:sync_task_type sync-task-type
|
||||
:initiated_by user-id
|
||||
:progress 0
|
||||
:started_at (mi/now)}
|
||||
:progress 0}
|
||||
additional-fields)))
|
||||
|
||||
(defn cancel-sync-task!
|
||||
@@ -63,10 +62,9 @@
|
||||
This signal will be checked in update-progress! to stop further processing. Note that the worker thread must
|
||||
manually check this flag rather than being interrupted, as interrupting Quartz threads can cause issues."
|
||||
[task-id]
|
||||
(remote-sync.db/update-task! task-id
|
||||
{:cancelled true
|
||||
:ended_at (mi/now)
|
||||
:error_message "Task cancelled"}))
|
||||
(remote-sync.db/end-task! task-id
|
||||
{:cancelled true
|
||||
:error_message "Task cancelled"}))
|
||||
|
||||
(defn update-progress!
|
||||
"Updates the progress of a sync task.
|
||||
@@ -80,9 +78,7 @@
|
||||
(when (true? (remote-sync.db/task-cancelled? task-id))
|
||||
(throw (ex-info "Remote sync task has been cancelled" {:task-id task-id
|
||||
:cancelled? true})))
|
||||
(remote-sync.db/update-task! task-id
|
||||
{:progress progress
|
||||
:last_progress_report_at (mi/now)}))
|
||||
(remote-sync.db/report-task-progress! task-id progress))
|
||||
|
||||
(def ^:private default-progress-throttle-ms
|
||||
"Minimum ms between throttled (non-boundary) progress writes."
|
||||
@@ -136,10 +132,9 @@
|
||||
Returns the number of rows updated (should be 1 if successful)."
|
||||
([task-id] (complete-sync-task! task-id nil))
|
||||
([task-id outcome]
|
||||
(remote-sync.db/update-task! task-id
|
||||
{:progress 1.0
|
||||
:ended_at (mi/now)
|
||||
:outcome outcome})))
|
||||
(remote-sync.db/end-task! task-id
|
||||
{:progress 1.0
|
||||
:outcome outcome})))
|
||||
|
||||
(defn fail-sync-task!
|
||||
"Marks a sync task as failed.
|
||||
@@ -148,9 +143,8 @@
|
||||
|
||||
Returns the number of rows updated (should be 1 if successful)."
|
||||
[task-id error-msg]
|
||||
(remote-sync.db/update-task! task-id
|
||||
{:ended_at (mi/now)
|
||||
:error_message error-msg}))
|
||||
(remote-sync.db/end-task! task-id
|
||||
{:error_message error-msg}))
|
||||
|
||||
(defn current-task
|
||||
"Gets the current active sync task.
|
||||
@@ -177,7 +171,7 @@
|
||||
[]
|
||||
(let [cutoff (t/minus (t/offset-date-time)
|
||||
(t/millis (setting/get :remote-sync-task-time-limit-ms)))]
|
||||
(remote-sync.db/supersede-stale-tasks! cutoff (mi/now))))
|
||||
(remote-sync.db/supersede-stale-tasks! cutoff)))
|
||||
|
||||
(defn most-recent-task
|
||||
"Gets the most recently run task, including currently running tasks.
|
||||
@@ -265,9 +259,8 @@
|
||||
|
||||
Returns the number of rows updated (should be 1 if successful)."
|
||||
[task-id conflicts]
|
||||
(remote-sync.db/update-task! task-id
|
||||
{:ended_at (mi/now)
|
||||
:conflicts conflicts}))
|
||||
(remote-sync.db/end-task! task-id
|
||||
{:conflicts conflicts}))
|
||||
|
||||
;;; ------------------------------------------- Hydration -------------------------------------------
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
(ns metabase-enterprise.remote-sync.schema
|
||||
"Malli schemas for remote sync API request and response bodies.")
|
||||
"Malli schemas for remote sync API request and response bodies."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
;;; ------------------------------------------- Task Schemas -------------------------------------------
|
||||
|
||||
@@ -164,3 +168,69 @@
|
||||
"Schema for POST /test-connection response."
|
||||
[:map
|
||||
[:status [:= :success]]])
|
||||
|
||||
(mr/def ::remote-sync-object
|
||||
"A RemoteSyncObject as selected from the app DB: every column of `:remote_sync_object`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:model_type [:or :keyword :string]]
|
||||
[:model_id [:maybe :int]]
|
||||
[:status [:or :keyword :string]]
|
||||
[:status_changed_at ms/TemporalInstant]
|
||||
[:model_name :string]
|
||||
[:model_collection_id [:maybe ::lib.schema.id/collection]]
|
||||
[:model_display [:maybe [:or :keyword :string]]]
|
||||
[:model_table_id [:maybe ::lib.schema.id/table]]
|
||||
[:model_table_name [:maybe :string]]
|
||||
[:file_path [:maybe :string]]
|
||||
[:content_hash [:maybe :string]]])
|
||||
|
||||
(mr/def ::remote-sync-object.update
|
||||
"What an update (or insert) of a RemoteSyncObject accepts: every column of `:remote_sync_object` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:model_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:model_id {:optional true} [:maybe :int]]
|
||||
[:status {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:status_changed_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:model_name {:optional true} [:maybe :string]]
|
||||
[:model_collection_id {:optional true} [:maybe ::lib.schema.id/collection]]
|
||||
[:model_display {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:model_table_id {:optional true} [:maybe ::lib.schema.id/table]]
|
||||
[:model_table_name {:optional true} [:maybe :string]]
|
||||
[:file_path {:optional true} [:maybe :string]]
|
||||
[:content_hash {:optional true} [:maybe :string]]])
|
||||
|
||||
(mr/def ::remote-sync-task.outcome
|
||||
"The `:outcome` column of a RemoteSyncTask, decoded."
|
||||
:map)
|
||||
|
||||
(mr/def ::remote-sync-task
|
||||
"A RemoteSyncTask as selected from the app DB: every column of `:remote_sync_task`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:sync_task_type [:or :keyword :string]]
|
||||
[:progress [:maybe number?]]
|
||||
[:cancelled :boolean]
|
||||
[:started_at ms/TemporalInstant]
|
||||
[:ended_at [:maybe ms/TemporalInstant]]
|
||||
[:last_progress_report_at ms/TemporalInstant]
|
||||
[:initiated_by [:maybe ::lib.schema.id/user]]
|
||||
[:error_message [:maybe :string]]
|
||||
[:version [:maybe :string]]
|
||||
[:conflicts [:maybe [:sequential :string]]]
|
||||
[:outcome [:maybe ::remote-sync-task.outcome]]])
|
||||
|
||||
(mr/def ::remote-sync-task.update
|
||||
"What an update (or insert) of a RemoteSyncTask accepts: every column of `:remote_sync_task` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:sync_task_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:progress {:optional true} [:maybe number?]]
|
||||
[:cancelled {:optional true} [:maybe :boolean]]
|
||||
[:started_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:ended_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:last_progress_report_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:initiated_by {:optional true} [:maybe ::lib.schema.id/user]]
|
||||
[:error_message {:optional true} [:maybe :string]]
|
||||
[:version {:optional true} [:maybe :string]]
|
||||
[:conflicts {:optional true} [:maybe [:sequential :string]]]
|
||||
[:outcome {:optional true} [:maybe ::remote-sync-task.outcome]]])
|
||||
|
||||
@@ -2,50 +2,63 @@
|
||||
"Application database queries for the replacement module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for model definitions."
|
||||
(:require
|
||||
[metabase-enterprise.replacement.schema :as replacement.schema]
|
||||
[metabase.app-db.core :as mdb]
|
||||
[metabase.dashboards.schema :as dashboards.schema]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.measures.schema :as measures.schema]
|
||||
[metabase.queries.schema :as queries.schema]
|
||||
[metabase.segments.schema :as segments.schema]
|
||||
[metabase.transforms.schema :as transforms.schema]
|
||||
[metabase.util.honey-sql-2 :as h2x]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[metabase.warehouse-schema.schema :as warehouse-schema.schema]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn run
|
||||
(mu/defn run
|
||||
"The ReplacementRun with `run-id`, or nil."
|
||||
[run-id]
|
||||
[run-id :- ms/PositiveInt]
|
||||
(t2/select-one :model/ReplacementRun :id run-id))
|
||||
|
||||
(defn runs
|
||||
(mu/defn runs
|
||||
"The ReplacementRuns, newest first, restricted to the `is-active` flag when given."
|
||||
[is-active]
|
||||
[is-active :- [:maybe :boolean]]
|
||||
(t2/select :model/ReplacementRun
|
||||
(cond-> {:order-by [[:start_time :desc]]}
|
||||
(some? is-active) (assoc :where [:= :is_active is-active]))))
|
||||
|
||||
(defn active-run
|
||||
(mu/defn active-run
|
||||
"The active ReplacementRun, or nil."
|
||||
[]
|
||||
(t2/select-one :model/ReplacementRun :is_active true))
|
||||
|
||||
(defn run-active-flag
|
||||
(mu/defn run-active-flag
|
||||
"The `:is_active` row of the ReplacementRun with `run-id`, or nil."
|
||||
[run-id]
|
||||
[run-id :- ms/PositiveInt]
|
||||
(t2/select-one [:model/ReplacementRun :is_active] :id run-id))
|
||||
|
||||
(defn insert-run!
|
||||
(mu/defn insert-run!
|
||||
"Insert `run` and return the new instance."
|
||||
[run]
|
||||
[run :- ::replacement.schema/replacement-run.update]
|
||||
(t2/insert-returning-instance! :model/ReplacementRun run))
|
||||
|
||||
(defn update-run!
|
||||
"Apply `changes` to the ReplacementRun with `run-id`."
|
||||
[run-id changes]
|
||||
(mu/defn update-run!
|
||||
"Apply `changes` to the ReplacementRun with `run-id`, returning the number updated."
|
||||
[run-id :- ms/PositiveInt
|
||||
changes :- ::replacement.schema/replacement-run.update]
|
||||
(t2/update! :model/ReplacementRun :id run-id changes))
|
||||
|
||||
(defn update-active-run!
|
||||
"Apply `changes` to the ReplacementRun with `run-id` if it is active."
|
||||
[run-id changes]
|
||||
(mu/defn update-active-run!
|
||||
"Apply `changes` to the ReplacementRun with `run-id` if it is active, returning the number updated."
|
||||
[run-id :- ms/PositiveInt
|
||||
changes :- ::replacement.schema/replacement-run.update]
|
||||
(t2/update! :model/ReplacementRun :id run-id :is_active true changes))
|
||||
|
||||
(defn time-out-active-runs-older-than!
|
||||
"Mark the active ReplacementRuns started more than `age` `unit`s ago as timed out."
|
||||
[age unit]
|
||||
(mu/defn time-out-active-runs-older-than!
|
||||
"Mark the active ReplacementRuns started more than `age` `unit`s ago as timed out, returning the number updated."
|
||||
[age :- ms/PositiveInt
|
||||
unit :- :keyword]
|
||||
(t2/update! :model/ReplacementRun
|
||||
:is_active true
|
||||
:start_time [:< (h2x/add-interval-honeysql-form (mdb/db-type) :%now (- age) unit)]
|
||||
@@ -54,147 +67,155 @@
|
||||
:end_time :%now
|
||||
:message "Timed out by metabase"}))
|
||||
|
||||
(defn cards-with-ids
|
||||
(mu/defn cards-with-ids
|
||||
"The Cards with `ids`."
|
||||
[ids]
|
||||
[ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select :model/Card :id [:in ids]))
|
||||
|
||||
(defn tables-with-ids
|
||||
(mu/defn tables-with-ids
|
||||
"The Tables with `ids`."
|
||||
[ids]
|
||||
[ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select :model/Table :id [:in ids]))
|
||||
|
||||
(defn dashboards-with-ids
|
||||
(mu/defn dashboards-with-ids
|
||||
"The Dashboards with `ids`."
|
||||
[ids]
|
||||
[ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select :model/Dashboard :id [:in ids]))
|
||||
|
||||
(defn transforms-with-ids
|
||||
(mu/defn transforms-with-ids
|
||||
"The Transforms with `ids`."
|
||||
[ids]
|
||||
[ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select :model/Transform :id [:in ids]))
|
||||
|
||||
(defn segments-with-ids
|
||||
(mu/defn segments-with-ids
|
||||
"The Segments with `ids`."
|
||||
[ids]
|
||||
[ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select :model/Segment :id [:in ids]))
|
||||
|
||||
(defn measures-with-ids
|
||||
(mu/defn measures-with-ids
|
||||
"The Measures with `ids`."
|
||||
[ids]
|
||||
[ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select :model/Measure :id [:in ids]))
|
||||
|
||||
(defn card
|
||||
(mu/defn card
|
||||
"The Card with `card-id`, or nil."
|
||||
[card-id]
|
||||
[card-id :- ::lib.schema.id/card]
|
||||
(t2/select-one :model/Card :id card-id))
|
||||
|
||||
(defn cards-by-id
|
||||
(mu/defn cards-by-id
|
||||
"A map of Card ID to Card for `card-ids`."
|
||||
[card-ids]
|
||||
[card-ids :- [:set ::lib.schema.id/card]]
|
||||
(t2/select-pk->fn identity :model/Card :id [:in card-ids]))
|
||||
|
||||
(defn card-database-id
|
||||
(mu/defn card-database-id
|
||||
"The Database ID of the Card with `card-id`."
|
||||
[card-id]
|
||||
[card-id :- ::lib.schema.id/card]
|
||||
(t2/select-one-fn :database_id :model/Card :id card-id))
|
||||
|
||||
(defn card-with-table-exists?
|
||||
(mu/defn card-with-table-exists?
|
||||
"Whether one of the Cards with `card-ids` is on the Table with `table-id`."
|
||||
[card-ids table-id]
|
||||
[card-ids :- [:set ::lib.schema.id/card]
|
||||
table-id :- ::lib.schema.id/table]
|
||||
(t2/exists? :model/Card :id [:in card-ids] :table_id table-id))
|
||||
|
||||
(defn update-card!
|
||||
"Apply `changes` to the Card with `card-id`."
|
||||
[card-id changes]
|
||||
(mu/defn update-card!
|
||||
"Apply `changes` to the Card with `card-id`, returning the number updated."
|
||||
[card-id :- ::lib.schema.id/card
|
||||
changes :- ::queries.schema/card.update]
|
||||
(t2/update! :model/Card card-id changes))
|
||||
|
||||
(defn transform
|
||||
(mu/defn transform
|
||||
"The Transform with `transform-id`, or nil."
|
||||
[transform-id]
|
||||
[transform-id :- ::lib.schema.id/transform]
|
||||
(t2/select-one :model/Transform :id transform-id))
|
||||
|
||||
(defn update-transform!
|
||||
"Apply `changes` to the Transform with `transform-id`."
|
||||
[transform-id changes]
|
||||
(mu/defn update-transform!
|
||||
"Apply `changes` to the Transform with `transform-id`, returning the number updated."
|
||||
[transform-id :- ::lib.schema.id/transform
|
||||
changes :- ::transforms.schema/transform.update]
|
||||
(t2/update! :model/Transform transform-id changes))
|
||||
|
||||
(defn segment
|
||||
(mu/defn segment
|
||||
"The Segment with `segment-id`, or nil."
|
||||
[segment-id]
|
||||
[segment-id :- ::lib.schema.id/segment]
|
||||
(t2/select-one :model/Segment :id segment-id))
|
||||
|
||||
(defn update-segment!
|
||||
"Apply `changes` to the Segment with `segment-id`."
|
||||
[segment-id changes]
|
||||
(mu/defn update-segment!
|
||||
"Apply `changes` to the Segment with `segment-id`, returning the number updated."
|
||||
[segment-id :- ::lib.schema.id/segment
|
||||
changes :- ::segments.schema/segment.update]
|
||||
(t2/update! :model/Segment segment-id changes))
|
||||
|
||||
(defn measure
|
||||
(mu/defn measure
|
||||
"The Measure with `measure-id`, or nil."
|
||||
[measure-id]
|
||||
[measure-id :- ::lib.schema.id/measure]
|
||||
(t2/select-one :model/Measure :id measure-id))
|
||||
|
||||
(defn update-measure!
|
||||
"Apply `changes` to the Measure with `measure-id`."
|
||||
[measure-id changes]
|
||||
(mu/defn update-measure!
|
||||
"Apply `changes` to the Measure with `measure-id`, returning the number updated."
|
||||
[measure-id :- ::lib.schema.id/measure
|
||||
changes :- ::measures.schema/measure.update]
|
||||
(t2/update! :model/Measure measure-id changes))
|
||||
|
||||
(defn dashboard
|
||||
(mu/defn dashboard
|
||||
"The Dashboard with `dashboard-id`, or nil."
|
||||
[dashboard-id]
|
||||
[dashboard-id :- ::lib.schema.id/dashboard]
|
||||
(t2/select-one :model/Dashboard :id dashboard-id))
|
||||
|
||||
(defn update-dashboard!
|
||||
"Apply `changes` to the Dashboard with `dashboard-id`."
|
||||
[dashboard-id changes]
|
||||
(mu/defn update-dashboard!
|
||||
"Apply `changes` to the Dashboard with `dashboard-id`, returning the number updated."
|
||||
[dashboard-id :- ::lib.schema.id/dashboard
|
||||
changes :- ::dashboards.schema/dashboard.update]
|
||||
(t2/update! :model/Dashboard dashboard-id changes))
|
||||
|
||||
(defn dashboard-cards
|
||||
(mu/defn dashboard-cards
|
||||
"The DashboardCards of the Dashboard with `dashboard-id`."
|
||||
[dashboard-id]
|
||||
[dashboard-id :- ::lib.schema.id/dashboard]
|
||||
(t2/select :model/DashboardCard :dashboard_id dashboard-id))
|
||||
|
||||
(defn update-dashboard-card!
|
||||
"Apply `changes` to the DashboardCard with `dashcard-id`."
|
||||
[dashcard-id changes]
|
||||
(mu/defn update-dashboard-card!
|
||||
"Apply `changes` to the DashboardCard with `dashcard-id`, returning the number updated."
|
||||
[dashcard-id :- ::lib.schema.id/dashcard
|
||||
changes :- ::dashboards.schema/dashboard-card.update]
|
||||
(t2/update! :model/DashboardCard dashcard-id changes))
|
||||
|
||||
(defn table-database-id
|
||||
(mu/defn table-database-id
|
||||
"The Database ID of the Table with `table-id`."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one-fn :db_id :model/Table :id table-id))
|
||||
|
||||
(defn active-fields-of-table
|
||||
(mu/defn active-fields-of-table
|
||||
"The active Fields of the Table with `table-id`."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select :model/Field :table_id table-id :active true))
|
||||
|
||||
(defn active-field-ids-of-table
|
||||
(mu/defn active-field-ids-of-table
|
||||
"The IDs of the active Fields of the Table with `table-id`."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-pks-set :model/Field :table_id table-id :active true))
|
||||
|
||||
(defn active-fk-to-fields-exists?
|
||||
(mu/defn active-fk-to-fields-exists?
|
||||
"Whether an active Field points at one of the Fields with `field-ids`."
|
||||
[field-ids]
|
||||
[field-ids :- [:set ::lib.schema.id/field]]
|
||||
(t2/exists? :model/Field :fk_target_field_id [:in field-ids] :active true))
|
||||
|
||||
(defn update-field!
|
||||
"Apply `changes` to the Field with `field-id`."
|
||||
[field-id changes]
|
||||
(mu/defn update-field!
|
||||
"Apply `changes` to the Field with `field-id`, returning the number updated."
|
||||
[field-id :- ::lib.schema.id/field
|
||||
changes :- ::warehouse-schema.schema/field.update]
|
||||
(t2/update! :model/Field field-id changes))
|
||||
|
||||
(defn sandbox-exists-for-table?
|
||||
(mu/defn sandbox-exists-for-table?
|
||||
"Whether a Sandbox is defined on the Table with `table-id`."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/exists? :model/Sandbox :table_id table-id))
|
||||
|
||||
(defn sandbox-card-ids
|
||||
(mu/defn sandbox-card-ids
|
||||
"The IDs of the Cards Sandboxes are built on."
|
||||
[]
|
||||
(t2/select-fn-set :card_id :model/Sandbox :card_id [:not= nil]))
|
||||
|
||||
(defn persisted-info-for-card
|
||||
(mu/defn persisted-info-for-card
|
||||
"The PersistedInfo of the Card with `card-id`, or nil."
|
||||
[card-id]
|
||||
[card-id :- ::lib.schema.id/card]
|
||||
(t2/select-one :model/PersistedInfo :card_id card-id))
|
||||
|
||||
@@ -13,6 +13,16 @@
|
||||
|
||||
(derive :model/ReplacementRun :metabase/model)
|
||||
|
||||
(def ^:private terminal-statuses
|
||||
#{:succeeded :failed :canceled :timeout})
|
||||
|
||||
(t2/define-before-update :model/ReplacementRun
|
||||
[run]
|
||||
(let [changes (t2/changes run)]
|
||||
(cond-> run
|
||||
(and (contains? terminal-statuses (keyword (:status changes))) (not (contains? changes :end_time)))
|
||||
(assoc :end_time (mi/now)))))
|
||||
|
||||
(t2/deftransforms :model/ReplacementRun
|
||||
{:status mi/transform-keyword
|
||||
:source_entity_type mi/transform-keyword
|
||||
@@ -48,8 +58,7 @@
|
||||
(replacement.db/update-active-run! run-id
|
||||
{:status :succeeded
|
||||
:progress 1.0
|
||||
:is_active nil
|
||||
:end_time :%now}))
|
||||
:is_active nil}))
|
||||
|
||||
(defn fail-run!
|
||||
"Mark the active run as failed."
|
||||
@@ -57,7 +66,6 @@
|
||||
(replacement.db/update-active-run! run-id
|
||||
{:status :failed
|
||||
:is_active nil
|
||||
:end_time :%now
|
||||
:message message}))
|
||||
|
||||
(defn cancel-run!
|
||||
@@ -66,7 +74,6 @@
|
||||
(replacement.db/update-active-run! run-id
|
||||
{:status :canceled
|
||||
:is_active nil
|
||||
:end_time :%now
|
||||
:message "Canceled by user"}))
|
||||
|
||||
(defn timeout-old-runs!
|
||||
|
||||
@@ -57,3 +57,34 @@
|
||||
[:success :boolean]
|
||||
[:errors {:optional true} [:sequential ::error-type]]
|
||||
[:column_mappings {:optional true} [:sequential ::column-mapping]]])
|
||||
|
||||
(mr/def ::replacement-run
|
||||
"A ReplacementRun as selected from the app DB: every column of `:source_replacement_run`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:source_entity_type [:or :keyword :string]]
|
||||
[:source_entity_id ms/PositiveInt]
|
||||
[:target_entity_type [:or :keyword :string]]
|
||||
[:target_entity_id ms/PositiveInt]
|
||||
[:status [:or :keyword :string]]
|
||||
[:is_active [:maybe :boolean]]
|
||||
[:progress [:maybe number?]]
|
||||
[:message [:maybe :string]]
|
||||
[:user_id [:maybe ::lib.schema.id/user]]
|
||||
[:start_time ms/TemporalInstant]
|
||||
[:end_time [:maybe ms/TemporalInstant]]])
|
||||
|
||||
(mr/def ::replacement-run.update
|
||||
"What an update (or insert) of a ReplacementRun accepts: every column of `:source_replacement_run` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:source_entity_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:source_entity_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:target_entity_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:target_entity_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:status {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:is_active {:optional true} [:maybe :boolean]]
|
||||
[:progress {:optional true} [:maybe number?]]
|
||||
[:message {:optional true} [:maybe :string]]
|
||||
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
|
||||
[:start_time {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:end_time {:optional true} [:maybe ms/TemporalInstant]]])
|
||||
|
||||
@@ -2,37 +2,46 @@
|
||||
"Application database queries for the sandbox module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for model definitions."
|
||||
(:require
|
||||
[malli.util :as mut]
|
||||
[metabase-enterprise.sandbox.schema :as sandbox.schema]
|
||||
[metabase.app-db.core :as mdb]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.queries.schema :as queries.schema]
|
||||
[metabase.users.schema :as users.schema]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn sandbox
|
||||
(mu/defn sandbox
|
||||
"The Sandbox with `sandbox-id`, or nil."
|
||||
[sandbox-id]
|
||||
[sandbox-id :- ::lib.schema.id/sandbox]
|
||||
(t2/select-one :model/Sandbox :id sandbox-id))
|
||||
|
||||
(defn sandboxes
|
||||
(mu/defn sandboxes
|
||||
"Every Sandbox, in ID order."
|
||||
[]
|
||||
(t2/select :model/Sandbox {:order-by [[:id :asc]]}))
|
||||
|
||||
(defn sandbox-for-group-and-table
|
||||
(mu/defn sandbox-for-group-and-table
|
||||
"The Sandbox of the group with `group-id` on the Table with `table-id`, or nil."
|
||||
[group-id table-id]
|
||||
[group-id :- ms/PositiveInt
|
||||
table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one :model/Sandbox :group_id group-id :table_id table-id))
|
||||
|
||||
(defn sandboxes-for-groups-and-table
|
||||
(mu/defn sandboxes-for-groups-and-table
|
||||
"The Sandboxes of the groups with `group-ids` on the Table with `table-id`."
|
||||
[group-ids table-id]
|
||||
[group-ids :- [:set ms/PositiveInt]
|
||||
table-id :- ::lib.schema.id/table]
|
||||
(t2/select :model/Sandbox :group_id [:in group-ids] :table_id table-id))
|
||||
|
||||
(defn sandboxes-using-card
|
||||
(mu/defn sandboxes-using-card
|
||||
"The `:id` and `:table_id` of the Sandboxes built on the Card with `card-id`."
|
||||
[card-id]
|
||||
[card-id :- ::lib.schema.id/card]
|
||||
(t2/select [:model/Sandbox :id :table_id] :card_id card-id))
|
||||
|
||||
(defn user-sandboxes-with-group-ids
|
||||
(mu/defn user-sandboxes-with-group-ids
|
||||
"The Sandboxes of the groups of the User with `user-id`, each with the `:group_id` of the membership."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select :model/Sandbox
|
||||
{:select [[:pgm.group_id :group_id]
|
||||
[:s.*]]
|
||||
@@ -41,10 +50,13 @@
|
||||
:where [:and
|
||||
[:= :pgm.user_id user-id]]}))
|
||||
|
||||
(defn sandboxes-with-table-info
|
||||
(mu/defn sandboxes-with-table-info
|
||||
"The group, Table, Database, and schema of the Sandboxes of the optional `group-id` or `group-ids` in the optional
|
||||
Database `db-id`, excluding the Database `excluded-db-id` when given."
|
||||
[group-id group-ids db-id excluded-db-id]
|
||||
[group-id :- [:maybe ms/PositiveInt]
|
||||
group-ids :- [:maybe [:sequential ms/PositiveInt]]
|
||||
db-id :- [:maybe ::lib.schema.id/database]
|
||||
excluded-db-id :- [:maybe ::lib.schema.id/database]]
|
||||
(t2/select :model/Sandbox
|
||||
{:select [:s.group_id :s.table_id :t.db_id :t.schema]
|
||||
:from [[:sandboxes :s]]
|
||||
@@ -55,10 +67,11 @@
|
||||
(when db-id [:= :t.db_id db-id])
|
||||
(when excluded-db-id [:not [:= :t.db_id excluded-db-id]])]}))
|
||||
|
||||
(defn candidate-sandboxes-for-groups-and-databases
|
||||
(mu/defn candidate-sandboxes-for-groups-and-databases
|
||||
"The `:id`, `:group_id`, `:table_id`, `:db_id`, and `:schema` of the Sandboxes of the groups with `group-ids` on
|
||||
Tables of the Databases with `db-ids`."
|
||||
[group-ids db-ids]
|
||||
[group-ids :- [:set ms/PositiveInt]
|
||||
db-ids :- [:set ::lib.schema.id/database]]
|
||||
(mdb/query
|
||||
{:select [[:sandboxes.id :id]
|
||||
[:sandboxes.group_id :group_id]
|
||||
@@ -72,47 +85,54 @@
|
||||
[:in :sandboxes.group_id group-ids]
|
||||
[:in :table.db_id db-ids]]}))
|
||||
|
||||
(defn insert-sandbox!
|
||||
(mu/defn insert-sandbox!
|
||||
"Insert `sandbox` and return the new instance."
|
||||
[sandbox]
|
||||
[sandbox :- [:map {:closed true}
|
||||
[:id {:optional true} ms/PositiveInt]
|
||||
[:table_id ::lib.schema.id/table]
|
||||
[:card_id {:optional true} [:maybe ::lib.schema.id/card]]
|
||||
[:group_id ms/PositiveInt]
|
||||
[:attribute_remappings {:optional true} [:maybe :map]]]]
|
||||
(first (t2/insert-returning-instances! :model/Sandbox sandbox)))
|
||||
|
||||
(defn update-sandbox!
|
||||
"Apply `changes` to the Sandbox with `sandbox-id`."
|
||||
[sandbox-id changes]
|
||||
(mu/defn update-sandbox!
|
||||
"Apply `changes` to the Sandbox with `sandbox-id`, returning the number updated."
|
||||
[sandbox-id :- ::lib.schema.id/sandbox
|
||||
changes :- (mut/select-keys ::sandbox.schema/sandbox.update [:card_id :attribute_remappings])]
|
||||
(t2/update! :model/Sandbox sandbox-id changes))
|
||||
|
||||
(defn delete-sandbox!
|
||||
"Delete the Sandbox with `sandbox-id`."
|
||||
[sandbox-id]
|
||||
(mu/defn delete-sandbox!
|
||||
"Delete the Sandbox with `sandbox-id`, returning the number deleted."
|
||||
[sandbox-id :- ::lib.schema.id/sandbox]
|
||||
(t2/delete! :model/Sandbox :id sandbox-id))
|
||||
|
||||
(defn delete-sandboxes!
|
||||
"Delete the Sandboxes with `sandbox-ids`."
|
||||
[sandbox-ids]
|
||||
(mu/defn delete-sandboxes!
|
||||
"Delete the Sandboxes with `sandbox-ids`, returning the number deleted."
|
||||
[sandbox-ids :- [:set ::lib.schema.id/sandbox]]
|
||||
(t2/delete! :model/Sandbox :id [:in sandbox-ids]))
|
||||
|
||||
(defn impersonations-for-groups
|
||||
(mu/defn impersonations-for-groups
|
||||
"The ConnectionImpersonations of the groups with `group-ids`."
|
||||
[group-ids]
|
||||
[group-ids :- [:set ms/PositiveInt]]
|
||||
(t2/select :model/ConnectionImpersonation :group_id [:in group-ids]))
|
||||
|
||||
(defn user-group-ids
|
||||
(mu/defn user-group-ids
|
||||
"The IDs of the groups of the User with `user-id`."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-fn-set :group_id :model/PermissionsGroupMembership :user_id user-id))
|
||||
|
||||
(defn personal-user
|
||||
(mu/defn personal-user
|
||||
"The personal User with `user-id`, or nil."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one :model/User :id user-id :type :personal))
|
||||
|
||||
(defn set-user-login-attributes!
|
||||
(mu/defn set-user-login-attributes!
|
||||
"Set the login attributes of the User with `user-id`, returning the number of rows updated."
|
||||
[user-id login-attributes]
|
||||
[user-id :- ::lib.schema.id/user
|
||||
login-attributes :- [:maybe users.schema/LoginAttributes]]
|
||||
(t2/update! :model/User user-id {:login_attributes login-attributes}))
|
||||
|
||||
(defn user-attributes-reducible
|
||||
(mu/defn user-attributes-reducible
|
||||
"Reducible merged JWT and login attribute maps of the Users that have any."
|
||||
[]
|
||||
(t2/select-fn-reducible (comp (partial apply merge)
|
||||
@@ -126,50 +146,53 @@
|
||||
[:not= :login_attributes nil]
|
||||
[:not= :login_attributes "{}"]]]}))
|
||||
|
||||
(defn table
|
||||
(mu/defn table
|
||||
"The Table with `table-id`, or nil."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one :model/Table :id table-id))
|
||||
|
||||
(defn tables-of-database
|
||||
(mu/defn tables-of-database
|
||||
"The `:id`, `:db_id`, and `:schema` of the Tables of the Database with `db-id`, restricted to `schema` when
|
||||
`schema-only?`."
|
||||
[db-id schema-only? schema]
|
||||
[db-id :- ::lib.schema.id/database
|
||||
schema-only? :- :boolean
|
||||
schema :- [:maybe :string]]
|
||||
(t2/select [:model/Table :id :db_id :schema]
|
||||
{:where [:and
|
||||
[:= :db_id db-id]
|
||||
(when schema-only?
|
||||
[:= :schema schema])]}))
|
||||
|
||||
(defn database-of-table
|
||||
(mu/defn database-of-table
|
||||
"The Database of the Table with `table-id`, or nil."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one :model/Database
|
||||
:id ^:allow-subquery {:select [:t.db_id]
|
||||
:from [[(t2/table-name :model/Table) :t]]
|
||||
:where [:= :t.id table-id]}))
|
||||
|
||||
(defn fields-of-table-named
|
||||
(mu/defn fields-of-table-named
|
||||
"The `:id` and `:name` of the Fields of the Table with `table-id` named one of `field-names`."
|
||||
[table-id field-names]
|
||||
[table-id :- ::lib.schema.id/table
|
||||
field-names :- [:set :string]]
|
||||
(t2/select [:model/Field :id :name] :table_id table-id :name [:in field-names]))
|
||||
|
||||
(defn cards-by-id
|
||||
(mu/defn cards-by-id
|
||||
"A map of Card ID to the query, result metadata, and schema of the Cards with `card-ids`."
|
||||
[card-ids]
|
||||
[card-ids :- [:set ::lib.schema.id/card]]
|
||||
(t2/select-pk->fn identity [:model/Card :id :dataset_query :result_metadata :card_schema] :id [:in card-ids]))
|
||||
|
||||
(defn cards-result-metadata
|
||||
(mu/defn cards-result-metadata
|
||||
"The `:id`, `:result_metadata`, and `:card_schema` of the Cards with `card-ids`."
|
||||
[card-ids]
|
||||
[card-ids :- [:set ::lib.schema.id/card]]
|
||||
(t2/select [:model/Card :id :result_metadata :card_schema] :id [:in card-ids]))
|
||||
|
||||
(defn card-result-metadata
|
||||
(mu/defn card-result-metadata
|
||||
"The result metadata of the Card with `card-id`."
|
||||
[card-id]
|
||||
[card-id :- ::lib.schema.id/card]
|
||||
(t2/select-one-fn :result_metadata :model/Card :id card-id))
|
||||
|
||||
(defn sandboxing-cards
|
||||
(mu/defn sandboxing-cards
|
||||
"The `:id`, `:dataset_query`, `:database_id`, and `:card_schema` of the Cards Sandboxes are built on."
|
||||
[]
|
||||
(t2/select :model/Card
|
||||
@@ -179,7 +202,8 @@
|
||||
:from [[(t2/table-name :model/Sandbox) :s]]
|
||||
:where [:= :s.card_id :c.id]}]}))
|
||||
|
||||
(defn set-card-result-metadata!
|
||||
"Set the result metadata of the Card with `card-id`."
|
||||
[card-id result-metadata]
|
||||
(mu/defn set-card-result-metadata!
|
||||
"Set the result metadata of the Card with `card-id`, returning the number updated."
|
||||
[card-id :- ::lib.schema.id/card
|
||||
result-metadata :- [:maybe ::queries.schema/card.result-metadata]]
|
||||
(t2/update! :model/Card card-id {:result_metadata result-metadata}))
|
||||
|
||||
@@ -32,6 +32,23 @@
|
||||
{:in (comp mi/json-in normalize-attribute-remappings)
|
||||
:out (comp normalize-attribute-remappings mi/json-out-without-keywordization)})
|
||||
|
||||
(mr/def ::sandbox.attribute-remappings
|
||||
"The `:attribute_remappings` column of a Sandbox, decoded."
|
||||
:map)
|
||||
|
||||
(mr/def ::sandbox
|
||||
[:map
|
||||
[:id ::lib.schema.id/sandbox]])
|
||||
"A Sandbox as selected from the app DB: every column of `:sandboxes`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:group_id ms/PositiveInt]
|
||||
[:table_id ::lib.schema.id/table]
|
||||
[:card_id [:maybe ::lib.schema.id/card]]
|
||||
[:attribute_remappings [:maybe ::sandbox.attribute-remappings]]])
|
||||
|
||||
(mr/def ::sandbox.update
|
||||
"What an update (or insert) of a Sandbox accepts: every column of `:sandboxes` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:group_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:table_id {:optional true} [:maybe ::lib.schema.id/table]]
|
||||
[:card_id {:optional true} [:maybe ::lib.schema.id/card]]
|
||||
[:attribute_remappings {:optional true} [:maybe ::sandbox.attribute-remappings]]])
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
"Application database queries for the scim module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for transactions."
|
||||
(:require
|
||||
[malli.util :as mut]
|
||||
[metabase.api-keys.schema :as api-keys.schema]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.permissions.schema :as permissions.schema]
|
||||
[metabase.users.schema :as users.schema]
|
||||
[metabase.util :as u]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(def ^:private user-columns
|
||||
@@ -13,19 +20,19 @@
|
||||
"Required columns when fetching groups for SCIM."
|
||||
[:model/PermissionsGroup :id :name :entity_id])
|
||||
|
||||
(defn scim-api-key
|
||||
(mu/defn scim-api-key
|
||||
"The SCIM ApiKey, or nil."
|
||||
[]
|
||||
(t2/select-one :model/ApiKey :scope :scim))
|
||||
|
||||
(defn delete-scim-api-keys!
|
||||
"Delete every SCIM ApiKey."
|
||||
(mu/defn delete-scim-api-keys!
|
||||
"Delete every SCIM ApiKey, returning the number deleted."
|
||||
[]
|
||||
(t2/delete! :model/ApiKey :scope :scim))
|
||||
|
||||
(defn insert-api-key!
|
||||
(mu/defn insert-api-key!
|
||||
"Insert `api-key` and return the new instance."
|
||||
[api-key]
|
||||
[api-key :- ::api-keys.schema/api-key.create]
|
||||
(t2/insert-returning-instance! :model/ApiKey api-key))
|
||||
|
||||
(defn- personal-user-expr
|
||||
@@ -33,55 +40,59 @@
|
||||
[:and [:= :type "personal"]
|
||||
(when email [:= :%lower.email (u/lower-case-en email)])])
|
||||
|
||||
(defn scim-user-by-entity-id
|
||||
(mu/defn scim-user-by-entity-id
|
||||
"The SCIM columns of the personal User with `entity-id`, or nil."
|
||||
[entity-id]
|
||||
[entity-id :- :string]
|
||||
(t2/select-one user-columns :entity_id entity-id {:where [:= :type "personal"]}))
|
||||
|
||||
(defn scim-user-by-email
|
||||
(mu/defn scim-user-by-email
|
||||
"The SCIM columns of the User with `email`, or nil."
|
||||
[email]
|
||||
[email :- :string]
|
||||
(t2/select-one user-columns :email (u/lower-case-en email)))
|
||||
|
||||
(defn scim-users
|
||||
(mu/defn scim-users
|
||||
"The SCIM columns of the personal Users, narrowed to the optional `email` (case-insensitive), paged by `limit`
|
||||
and `offset` in ID order."
|
||||
[email limit offset]
|
||||
[email :- [:maybe :string]
|
||||
limit :- [:maybe ms/PositiveInt]
|
||||
offset :- [:maybe ms/IntGreaterThanOrEqualToZero]]
|
||||
(t2/select user-columns
|
||||
{:where (personal-user-expr email)
|
||||
:limit limit
|
||||
:offset offset
|
||||
:order-by [[:id :asc]]}))
|
||||
|
||||
(defn scim-user-count
|
||||
(mu/defn scim-user-count
|
||||
"The number of personal Users, narrowed to the optional `email` (case-insensitive)."
|
||||
[email]
|
||||
[email :- [:maybe :string]]
|
||||
(t2/count :model/User {:where (personal-user-expr email)}))
|
||||
|
||||
(defn user-email-exists?
|
||||
(mu/defn user-email-exists?
|
||||
"Whether a User with `email` (case-insensitive) exists."
|
||||
[email]
|
||||
[email :- :string]
|
||||
(t2/exists? :model/User :%lower.email (u/lower-case-en email)))
|
||||
|
||||
(defn user-ids-by-entity-ids
|
||||
(mu/defn user-ids-by-entity-ids
|
||||
"The IDs of the Users with `entity-ids`."
|
||||
[entity-ids]
|
||||
[entity-ids :- [:sequential :string]]
|
||||
(t2/select-fn-set :id :model/User {:where [:in :entity_id entity-ids]}))
|
||||
|
||||
(defn insert-user!
|
||||
"Insert the User `row`."
|
||||
[row]
|
||||
(mu/defn insert-user!
|
||||
"Insert the User `row`, returning the number inserted."
|
||||
[row :- ::users.schema/user.update]
|
||||
(t2/insert! :model/User row))
|
||||
|
||||
(defn update-user!
|
||||
"Apply `changes` to the User with `user-id`."
|
||||
[user-id changes]
|
||||
(mu/defn update-user!
|
||||
"Apply `changes` to the User with `user-id`, returning the number updated."
|
||||
[user-id :- ::lib.schema.id/user
|
||||
changes :- ::users.schema/user.update]
|
||||
(t2/update! :model/User user-id changes))
|
||||
|
||||
(defn user-group-memberships
|
||||
(mu/defn user-group-memberships
|
||||
"Rows of User ID, group name, and group entity ID for the memberships of the Users with `user-ids`, excluding the
|
||||
groups with `excluded-group-ids`."
|
||||
[user-ids excluded-group-ids]
|
||||
[user-ids :- [:sequential ::lib.schema.id/user]
|
||||
excluded-group-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select [:model/PermissionsGroupMembership :pgm.user_id :pg.name :pg.entity_id]
|
||||
{:from [[:permissions_group_membership :pgm]]
|
||||
:join [[:permissions_group :pg] [:= :pg.id :group_id]]
|
||||
@@ -95,50 +106,56 @@
|
||||
(map (fn [group-id] [:not= :id group-id]))
|
||||
excluded-group-ids))
|
||||
|
||||
(defn scim-group-by-entity-id
|
||||
(mu/defn scim-group-by-entity-id
|
||||
"The SCIM columns of the PermissionsGroup with `entity-id` other than `excluded-group-ids`, or nil."
|
||||
[entity-id excluded-group-ids]
|
||||
[entity-id :- :string
|
||||
excluded-group-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select-one group-columns :entity_id entity-id {:where (manageable-group-expr excluded-group-ids nil)}))
|
||||
|
||||
(defn scim-groups
|
||||
(mu/defn scim-groups
|
||||
"The SCIM columns of the PermissionsGroups other than `excluded-group-ids`, narrowed to the optional `group-name`,
|
||||
paged by `limit` and `offset` in ID order."
|
||||
[excluded-group-ids group-name limit offset]
|
||||
[excluded-group-ids :- [:sequential ms/PositiveInt]
|
||||
group-name :- [:maybe :string]
|
||||
limit :- [:maybe ms/PositiveInt]
|
||||
offset :- [:maybe ms/IntGreaterThanOrEqualToZero]]
|
||||
(t2/select group-columns
|
||||
{:where (manageable-group-expr excluded-group-ids group-name)
|
||||
:limit limit
|
||||
:offset offset
|
||||
:order-by [[:id :asc]]}))
|
||||
|
||||
(defn scim-group-count
|
||||
(mu/defn scim-group-count
|
||||
"The number of PermissionsGroups other than `excluded-group-ids`, narrowed to the optional `group-name`."
|
||||
[excluded-group-ids group-name]
|
||||
[excluded-group-ids :- [:sequential ms/PositiveInt]
|
||||
group-name :- [:maybe :string]]
|
||||
(t2/count :model/PermissionsGroup {:where (manageable-group-expr excluded-group-ids group-name)}))
|
||||
|
||||
(defn group-name-exists?
|
||||
(mu/defn group-name-exists?
|
||||
"Whether a PermissionsGroup with `group-name` (case-insensitive) exists."
|
||||
[group-name]
|
||||
[group-name :- :string]
|
||||
(t2/exists? :model/PermissionsGroup :%lower.name (u/lower-case-en group-name)))
|
||||
|
||||
(defn insert-group!
|
||||
(mu/defn insert-group!
|
||||
"Insert `group` and return the new instance."
|
||||
[group]
|
||||
[group :- (mut/select-keys ::permissions.schema/permissions-group.update [:name :entity_id :magic_group_type :is_tenant_group])]
|
||||
(first (t2/insert-returning-instances! :model/PermissionsGroup group)))
|
||||
|
||||
(defn update-group!
|
||||
"Apply `changes` to the PermissionsGroup with `group-id`."
|
||||
[group-id changes]
|
||||
(mu/defn update-group!
|
||||
"Apply `changes` to the PermissionsGroup with `group-id`, returning the number updated."
|
||||
[group-id :- ms/PositiveInt
|
||||
changes :- (mut/select-keys ::permissions.schema/permissions-group.update [:name :entity_id :magic_group_type :is_tenant_group])]
|
||||
(t2/update! :model/PermissionsGroup group-id changes))
|
||||
|
||||
(defn delete-group!
|
||||
"Delete the PermissionsGroup with `group-id`."
|
||||
[group-id]
|
||||
(mu/defn delete-group!
|
||||
"Delete the PermissionsGroup with `group-id`, returning the number deleted."
|
||||
[group-id :- ms/PositiveInt]
|
||||
(t2/delete! :model/PermissionsGroup group-id))
|
||||
|
||||
(defn group-members
|
||||
(mu/defn group-members
|
||||
"Rows of group ID, member email, and member entity ID for the memberships of the PermissionsGroups with
|
||||
`group-ids`."
|
||||
[group-ids]
|
||||
[group-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select [:model/PermissionsGroupMembership :pgm.group_id :u.email :u.entity_id]
|
||||
{:from [[:permissions_group_membership :pgm]]
|
||||
:join [[:core_user :u] [:= :u.id :pgm.user_id]]
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
(defenterprise scorers
|
||||
"Return the select-item expressions used to calculate the score for each search result."
|
||||
:feature :none
|
||||
[search-ctx]
|
||||
(merge (appdb.scoring/base-scorers search-ctx) (additional-scorers)))
|
||||
[search-ctx view-count-percentiles]
|
||||
(merge (appdb.scoring/base-scorers search-ctx view-count-percentiles) (additional-scorers)))
|
||||
|
||||
;; ------------ LEGACY ----------
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
[metabase-enterprise.security-center.db :as security-center.db]
|
||||
[metabase-enterprise.security-center.models.security-advisory :as security-advisory]
|
||||
[metabase-enterprise.security-center.notification :as notification]
|
||||
[metabase-enterprise.security-center.schema :as security-center.schema]
|
||||
[metabase-enterprise.security-center.settings :as settings]
|
||||
[metabase-enterprise.security-center.task.sync-advisories :as sync-advisories]
|
||||
[metabase.api.common :as api]
|
||||
@@ -13,6 +12,7 @@
|
||||
[metabase.api.routes.common :as routes.common :refer [+auth]]
|
||||
[metabase.notification.models :as models.notification]
|
||||
[metabase.premium-features.core :as premium-features]
|
||||
[metabase.security-center.schema :as security-center.schema]
|
||||
[metabase.util.i18n :refer [tru]]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2])
|
||||
|
||||
@@ -2,60 +2,78 @@
|
||||
"Application database queries for the security-center module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for model definitions and hydration methods."
|
||||
(:require
|
||||
[malli.util :as mut]
|
||||
[metabase.app-db.core :as mdb]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.security-center.schema :as security-center.schema]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn advisory
|
||||
(mu/defn advisory
|
||||
"The SecurityAdvisory with `id`, or nil."
|
||||
[id]
|
||||
[id :- ms/PositiveInt]
|
||||
(t2/select-one :model/SecurityAdvisory :id id))
|
||||
|
||||
(defn advisory-by-advisory-id
|
||||
(mu/defn advisory-by-advisory-id
|
||||
"The SecurityAdvisory with the external `advisory-id`, or nil."
|
||||
[advisory-id]
|
||||
[advisory-id :- :string]
|
||||
(t2/select-one :model/SecurityAdvisory :advisory_id advisory-id))
|
||||
|
||||
(defn max-advisory-updated-at
|
||||
(mu/defn max-advisory-updated-at
|
||||
"The latest `updated_at` across every SecurityAdvisory, or nil when there are none."
|
||||
[]
|
||||
(:updated_at (t2/query-one {:select [[[:max :updated_at] :updated_at]]
|
||||
:from [:security_advisory]})))
|
||||
|
||||
(defn advisories-newest-first
|
||||
(mu/defn advisories-newest-first
|
||||
"Every SecurityAdvisory, newest first."
|
||||
[]
|
||||
(t2/select :model/SecurityAdvisory {:order-by [[:published_at :desc]]}))
|
||||
|
||||
(defn advisories-reducible
|
||||
(mu/defn advisories-reducible
|
||||
"Reducible SecurityAdvisories."
|
||||
[]
|
||||
(t2/reducible-select :model/SecurityAdvisory))
|
||||
|
||||
(defn advisories-with-statuses-reducible
|
||||
(mu/defn advisories-with-statuses-reducible
|
||||
"Reducible severity and acknowledgement time of the SecurityAdvisories whose match status is one of `statuses`."
|
||||
[statuses]
|
||||
[statuses :- [:set [:or :keyword :string]]]
|
||||
(t2/reducible-select [:model/SecurityAdvisory :severity :acknowledged_at] :match_status [:in statuses]))
|
||||
|
||||
(defn unacknowledged-advisories-with-statuses
|
||||
(mu/defn unacknowledged-advisories-with-statuses
|
||||
"The unacknowledged SecurityAdvisories whose match status is one of `statuses`."
|
||||
[statuses]
|
||||
[statuses :- [:sequential [:or :keyword :string]]]
|
||||
(t2/select :model/SecurityAdvisory :acknowledged_at nil :match_status [:in statuses]))
|
||||
|
||||
(defn unacknowledged-advisories-by-advisory-ids
|
||||
(mu/defn unacknowledged-advisories-by-advisory-ids
|
||||
"The unacknowledged SecurityAdvisories with the external `advisory-ids`."
|
||||
[advisory-ids]
|
||||
[advisory-ids :- [:set :string]]
|
||||
(t2/select :model/SecurityAdvisory :advisory_id [:in advisory-ids] :acknowledged_at nil))
|
||||
|
||||
(defn update-advisory!
|
||||
"Apply `changes` to the SecurityAdvisory with `id`."
|
||||
[id changes]
|
||||
(mu/defn update-advisory!
|
||||
"Apply `changes` to the SecurityAdvisory with `id`, returning the number updated."
|
||||
[id :- ms/PositiveInt
|
||||
changes :- (mut/select-keys ::security-center.schema/security-advisory.update [:match_status :last_evaluated_at :acknowledged_by :acknowledged_at :last_notified_at])]
|
||||
(t2/update! :model/SecurityAdvisory id changes))
|
||||
|
||||
(defn upsert-advisory!
|
||||
(mu/defn record-advisory-evaluation!
|
||||
"Apply `changes` to the SecurityAdvisory with `id` and stamp `last_evaluated_at` with now, returning the number
|
||||
updated."
|
||||
[id :- ms/PositiveInt
|
||||
changes :- (mut/select-keys ::security-center.schema/security-advisory.update [:match_status :acknowledged_by :acknowledged_at])]
|
||||
(t2/update! :model/SecurityAdvisory id (assoc changes :last_evaluated_at :%now)))
|
||||
|
||||
(mu/defn record-advisory-notification!
|
||||
"Set `last_notified_at` of the SecurityAdvisory with `id` to now, returning the number updated."
|
||||
[id :- ms/PositiveInt]
|
||||
(t2/update! :model/SecurityAdvisory id {:last_notified_at :%now}))
|
||||
|
||||
(mu/defn upsert-advisory!
|
||||
"Insert or update a SecurityAdvisory by `:advisory_id`. On insert, `:match_status` starts as `:unknown` until the
|
||||
matching engine evaluates it. On update, sets `advisory`'s columns, leaving `:match_status`, `:last_evaluated_at`,
|
||||
and acknowledgement fields (which `advisory` doesn't include) untouched."
|
||||
[advisory]
|
||||
[advisory :- ::security-center.schema/security-advisory.update]
|
||||
(mdb/update-or-insert! :model/SecurityAdvisory
|
||||
{:advisory_id (:advisory_id advisory)}
|
||||
(fn [existing]
|
||||
@@ -63,7 +81,7 @@
|
||||
advisory
|
||||
(assoc advisory :match_status :unknown)))))
|
||||
|
||||
(defn user-summaries-by-id
|
||||
(mu/defn user-summaries-by-id
|
||||
"A map of ID to the ID, names, and email of the Users with `user-ids`."
|
||||
[user-ids]
|
||||
[user-ids :- [:set ::lib.schema.id/user]]
|
||||
(t2/select-fn->fn :id identity [:model/User :id :first_name :last_name :email] :id [:in user-ids]))
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
[clojure.set :as set]
|
||||
[java-time.api :as t]
|
||||
[metabase-enterprise.security-center.db :as security-center.db]
|
||||
[metabase-enterprise.security-center.schema :as security-center.schema]
|
||||
[metabase.config.core :as config]
|
||||
[metabase.premium-features.core :as premium-features]
|
||||
[metabase.security-center.schema :as security-center.schema]
|
||||
[metabase.util.json :as json]
|
||||
[metabase.util.log :as log]
|
||||
[metabase.util.malli :as mu]
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
and combines with version checks to determine match status."
|
||||
(:require
|
||||
[metabase-enterprise.security-center.db :as security-center.db]
|
||||
[metabase-enterprise.security-center.schema :as schema]
|
||||
[metabase.app-db.core :as mdb]
|
||||
[metabase.config.core :as config]
|
||||
[metabase.driver.sql-jdbc.execute :as sql-jdbc.execute]
|
||||
[metabase.models.interface :as mi]
|
||||
[metabase.security-center.schema :as schema]
|
||||
[metabase.util.connection :as u.connection]
|
||||
[metabase.util.log :as log]
|
||||
[metabase.util.malli :as mu]
|
||||
@@ -148,11 +147,10 @@
|
||||
reactivated? (and (#{:active :error} match-status)
|
||||
currently-unaffected
|
||||
(some? (:acknowledged_at advisory)))]
|
||||
(security-center.db/update-advisory! (:id advisory)
|
||||
(cond-> {:match_status match-status
|
||||
:last_evaluated_at (mi/now)}
|
||||
reactivated? (assoc :acknowledged_at nil
|
||||
:acknowledged_by nil))))))))
|
||||
(security-center.db/record-advisory-evaluation! (:id advisory)
|
||||
(cond-> {:match_status match-status}
|
||||
reactivated? (assoc :acknowledged_at nil
|
||||
:acknowledged_by nil))))))))
|
||||
|
||||
(defn evaluate-all-advisories!
|
||||
"Re-evaluate every advisory, including acknowledged ones — an acked
|
||||
@@ -166,6 +164,5 @@
|
||||
(evaluate-advisory! advisory instance-version)
|
||||
(catch Exception e
|
||||
(log/warnf "Error evaluating advisory %s: %s" (:advisory_id advisory) (ex-message e))
|
||||
(security-center.db/update-advisory! (:id advisory)
|
||||
{:match_status :error
|
||||
:last_evaluated_at (mi/now)}))))))))
|
||||
(security-center.db/record-advisory-evaluation! (:id advisory)
|
||||
{:match_status :error}))))))))
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
[advisory]
|
||||
(assoc advisory :fetched_at (mi/now)))
|
||||
|
||||
(t2/define-before-update :model/SecurityAdvisory
|
||||
[advisory]
|
||||
(let [changes (t2/changes advisory)]
|
||||
(cond-> advisory
|
||||
(and (some? (:acknowledged_by changes)) (not (contains? changes :acknowledged_at)))
|
||||
(assoc :acknowledged_at (mi/now)))))
|
||||
|
||||
(methodical/defmethod t2/batched-hydrate [:model/SecurityAdvisory :acknowledged_by_user]
|
||||
"Hydrate `:acknowledged_by_user` from the `:acknowledged_by` FK to a User map with `:id`, `:common_name`, and `:email`."
|
||||
[_model k advisories]
|
||||
@@ -43,18 +50,15 @@
|
||||
[advisory user-id]
|
||||
(when (:acknowledged_at advisory)
|
||||
(throw (ex-info "Advisory already acknowledged" {:status-code 409})))
|
||||
(let [now (mi/now)]
|
||||
(security-center.db/update-advisory! (:id advisory)
|
||||
{:acknowledged_by user-id
|
||||
:acknowledged_at now})
|
||||
(events/publish-event! :event/security-advisory-acknowledge
|
||||
{:object advisory
|
||||
:user-id user-id})
|
||||
(analytics/track-event! :snowplow/simple_event
|
||||
{:event "security_advisory_acknowledged"
|
||||
:event_detail (name (:severity advisory))})
|
||||
(-> (security-center.db/advisory (:id advisory))
|
||||
(t2/hydrate :acknowledged_by_user))))
|
||||
(security-center.db/update-advisory! (:id advisory) {:acknowledged_by user-id})
|
||||
(events/publish-event! :event/security-advisory-acknowledge
|
||||
{:object advisory
|
||||
:user-id user-id})
|
||||
(analytics/track-event! :snowplow/simple_event
|
||||
{:event "security_advisory_acknowledged"
|
||||
:event_detail (name (:severity advisory))})
|
||||
(-> (security-center.db/advisory (:id advisory))
|
||||
(t2/hydrate :acknowledged_by_user)))
|
||||
|
||||
(defn acknowledge-many!
|
||||
"Acknowledge multiple security advisories by their advisory_id strings. Skips already-acknowledged
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
[metabase.analytics.core :as analytics]
|
||||
[metabase.channel.settings :as channel.settings]
|
||||
[metabase.events.core :as events]
|
||||
[metabase.models.interface :as mi]
|
||||
[metabase.notification.core :as notification]
|
||||
[metabase.permissions.core :as perms]
|
||||
[metabase.settings.core :as setting]
|
||||
@@ -169,7 +168,7 @@
|
||||
(try
|
||||
(notification/send-notification! notif :notification/sync? true)
|
||||
(track-notification-sent! notif triggered-from "success")
|
||||
(security-center.db/update-advisory! (:id advisory) {:last_notified_at (mi/now)})
|
||||
(security-center.db/record-advisory-notification! (:id advisory))
|
||||
(catch Exception e
|
||||
(track-notification-sent! notif triggered-from "failure")
|
||||
(throw e))))))
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
(ns metabase-enterprise.security-center.schema
|
||||
"Malli schemas for the Security Center module."
|
||||
(:require
|
||||
[metabase.util.malli.registry :as mr]))
|
||||
|
||||
(mr/def ::advisory-id
|
||||
[:string {:min 1}])
|
||||
|
||||
(mr/def ::semver
|
||||
[:re #"^\d+(?:\.\d+)*$"])
|
||||
|
||||
(mr/def ::severity
|
||||
[:enum :critical :high :medium :low])
|
||||
|
||||
(mr/def ::match-status
|
||||
[:enum :unknown :active :resolved :not_affected :error])
|
||||
|
||||
(mr/def ::version-range
|
||||
"A single affected version range with inclusive min and exclusive fixed."
|
||||
[:map
|
||||
[:min ::semver]
|
||||
[:fixed ::semver]])
|
||||
|
||||
(mr/def ::affected-versions
|
||||
[:sequential ::version-range])
|
||||
|
||||
(mr/def ::download-jar-url
|
||||
"A downloadable JAR for a given fixed version."
|
||||
[:map
|
||||
[:version ::semver]
|
||||
[:url :string]])
|
||||
|
||||
(mr/def ::download-jar-urls
|
||||
[:sequential ::download-jar-url])
|
||||
|
||||
(mr/def ::matching-query
|
||||
"HoneySQL query keyed by dialect. nil means affects all instances.
|
||||
Stored as EDN to preserve keywords that HoneySQL requires for identifiers/operators."
|
||||
[:maybe [:map-of :keyword :map]])
|
||||
@@ -0,0 +1,14 @@
|
||||
(ns metabase-enterprise.semantic-search.appdb-scoring
|
||||
"The app-DB based scorers of semantic search results. Expression building only:
|
||||
`metabase-enterprise.semantic-search.db` runs the query, so this namespace must not require it."
|
||||
(:require
|
||||
[metabase.search.config :as search.config]
|
||||
[metabase.search.scoring :as search.scoring]))
|
||||
|
||||
(defn appdb-scorers
|
||||
"The appdb-based scorers for search ranking results. Like `base-scorers`, but for scorers that need to query the appdb."
|
||||
[search-ctx]
|
||||
(when-not (search.scoring/no-scoring-required? search-ctx)
|
||||
{:bookmarked search.scoring/bookmark-score-expr
|
||||
:user-recency (search.scoring/inverse-duration
|
||||
(search.scoring/user-recency-expr search-ctx) [:now] search.config/stale-time-in-days)}))
|
||||
@@ -2,21 +2,27 @@
|
||||
"Application database queries for the semantic-search module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for model definitions and hydration methods."
|
||||
(:require
|
||||
[malli.util :as mut]
|
||||
[metabase-enterprise.semantic-search.appdb-scoring :as appdb-scoring]
|
||||
[metabase-enterprise.semantic-search.schema :as semantic-search.schema]
|
||||
[metabase.app-db.core :as mdb]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.search.scoring :as search.scoring]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn library-root-collections
|
||||
(mu/defn library-root-collections
|
||||
"The ID and type of the top-level Collections whose type is one of `types`."
|
||||
[types]
|
||||
[types :- [:sequential :string]]
|
||||
(t2/select [:model/Collection :id :type] :type [:in types] :location "/"))
|
||||
|
||||
(defn descendant-collection-ids
|
||||
(mu/defn descendant-collection-ids
|
||||
"The IDs of the Collections under the top-level Collection with `root-id`."
|
||||
[root-id]
|
||||
[root-id :- ms/PositiveInt]
|
||||
(t2/select-pks-set :model/Collection :location [:like (str "/" root-id "/%")]))
|
||||
|
||||
(defn curated-tables-reducible
|
||||
(mu/defn curated-tables-reducible
|
||||
"Reducible ID, published flag, data layer, and data authority of the active Tables that are published or
|
||||
authoritative."
|
||||
[]
|
||||
@@ -26,38 +32,39 @@
|
||||
[:or [:= :is_published true]
|
||||
[:= :data_authority ^:allow-raw-sql [:inline "authoritative"]]]]}))
|
||||
|
||||
(defn official-collection-ids
|
||||
(mu/defn official-collection-ids
|
||||
"The IDs of the official Collections."
|
||||
[]
|
||||
(t2/select-pks-set :model/Collection :authority_level :official))
|
||||
|
||||
(defn unarchived-dashboard-ids-in-collections-reducible
|
||||
(mu/defn unarchived-dashboard-ids-in-collections-reducible
|
||||
"Reducible `:id` rows of the unarchived Dashboards in the Collections with `collection-ids`."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:sequential ::lib.schema.id/collection]]
|
||||
(t2/reducible-select [:model/Dashboard :id]
|
||||
{:where [:and [:= :archived false]
|
||||
[:in :collection_id collection-ids]]}))
|
||||
|
||||
(defn collection-owners-and-locations
|
||||
(mu/defn collection-owners-and-locations
|
||||
"The ID, owner, and location of the Collections with `collection-ids`."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:set ::lib.schema.id/collection]]
|
||||
(t2/select [:model/Collection :id :personal_owner_id :location] :id [:in collection-ids]))
|
||||
|
||||
(defn personal-collection-owners
|
||||
(mu/defn personal-collection-owners
|
||||
"The ID and owner of the personal Collections among `collection-ids`."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:set ::lib.schema.id/collection]]
|
||||
(t2/select [:model/Collection :id :personal_owner_id]
|
||||
:id [:in collection-ids]
|
||||
:personal_owner_id [:not= nil]))
|
||||
|
||||
(defn collection-locations
|
||||
(mu/defn collection-locations
|
||||
"The ID and location of the raw collection rows with `collection-ids`."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:sequential ::lib.schema.id/collection]]
|
||||
(t2/select [:collection :id :location] :id [:in collection-ids]))
|
||||
|
||||
(defn instances
|
||||
(mu/defn instances
|
||||
"The instances of `model` with `ids`."
|
||||
[model ids]
|
||||
[model :- :keyword
|
||||
ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select model :id [:in ids]))
|
||||
|
||||
(defn- search-doc-select
|
||||
@@ -78,21 +85,27 @@
|
||||
[:search_index.model :model]]
|
||||
:from [:search_index]})
|
||||
|
||||
(defn appdb-scored-rows
|
||||
"The `:id`/`:model` rows of `search-results` (each `{:id :model}`) augmented with the SELECT expressions of
|
||||
`scorers` (a map of scorer key to SELECT expression, see
|
||||
`metabase-enterprise.semantic-search.scoring/appdb-scorers`) evaluated under `search-ctx`, joining bookmark
|
||||
tables when `:bookmarked` is among `scorers`."
|
||||
[search-results search-ctx scorers]
|
||||
(t2/query (cond-> (search.scoring/with-scores search-ctx scorers (search-index-select search-results))
|
||||
(:bookmarked scorers) (search.scoring/join-bookmarks (:current-user-id search-ctx)))))
|
||||
(mu/defn appdb-scored-rows
|
||||
"The `:id`/`:model` rows of `search-results` augmented with the app-DB scorers of `search-ctx` (see
|
||||
`metabase-enterprise.semantic-search.appdb-scoring/appdb-scorers`), joining the bookmark tables when `:bookmarked`
|
||||
is among them."
|
||||
[search-results :- [:sequential [:map {:closed true}
|
||||
[:id [:or :string ms/PositiveInt]]
|
||||
[:model :string]]]
|
||||
search-ctx :- [:map
|
||||
[:current-user-id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:context {:optional true} [:maybe :keyword]]
|
||||
[:weights {:optional true} [:maybe [:map-of :keyword number?]]]]]
|
||||
(let [scorers (appdb-scoring/appdb-scorers search-ctx)]
|
||||
(t2/query (cond-> (search.scoring/with-scores search-ctx scorers (search-index-select search-results))
|
||||
(:bookmarked scorers) (search.scoring/join-bookmarks (:current-user-id search-ctx))))))
|
||||
|
||||
(defn insert-token-tracking!
|
||||
(mu/defn insert-token-tracking!
|
||||
"Insert the SemanticSearchTokenTracking `row`."
|
||||
[row]
|
||||
[row :- (mut/select-keys ::semantic-search.schema/semantic-search-token-tracking.update [:model_name :request_type :total_tokens])]
|
||||
(t2/insert! :model/SemanticSearchTokenTracking row))
|
||||
|
||||
(defn delete-token-tracking-created-before!
|
||||
(mu/defn delete-token-tracking-created-before!
|
||||
"Delete the SemanticSearchTokenTracking rows created before `cutoff`."
|
||||
[cutoff]
|
||||
[cutoff :- ms/TemporalInstant]
|
||||
(t2/delete! :model/SemanticSearchTokenTracking {:where [:< :created_at cutoff]}))
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
[honey.sql :as sql]
|
||||
[honey.sql.helpers :as sql.helpers]
|
||||
[java-time.api :as t]
|
||||
[metabase-enterprise.semantic-search.appdb-scoring :as appdb-scoring]
|
||||
[metabase-enterprise.semantic-search.db :as semantic-search.db]
|
||||
;; TODO: extract schema code to go under db.migration
|
||||
[metabase-enterprise.semantic-search.embedding :as embedding]
|
||||
@@ -1261,7 +1262,7 @@
|
||||
(mapv search/collapse-id)))
|
||||
filter-time-ms (u/since-ms filter-timer)
|
||||
|
||||
appdb-scorers (scoring/appdb-scorers search-context)
|
||||
appdb-scorers (appdb-scoring/appdb-scorers search-context)
|
||||
appdb-scores-timer (u/start-timer)
|
||||
final-results (->> filtered-results
|
||||
(scoring/with-appdb-scores search-context appdb-scorers weights))
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
(ns metabase-enterprise.semantic-search.schema
|
||||
"Malli schemas for the semantic-search module."
|
||||
(:require
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
(mr/def ::semantic-search-token-tracking
|
||||
"A SemanticSearchTokenTracking as selected from the app DB: every column of `:semantic_search_token_tracking`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:model_name :string]
|
||||
[:request_type [:maybe [:or :keyword :string]]]
|
||||
[:created_at ms/TemporalInstant]
|
||||
[:total_tokens :int]])
|
||||
|
||||
(mr/def ::semantic-search-token-tracking.update
|
||||
"What an update (or insert) of a SemanticSearchTokenTracking accepts: every column of `:semantic_search_token_tracking` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:model_name {:optional true} [:maybe :string]]
|
||||
[:request_type {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:total_tokens {:optional true} [:maybe :int]]])
|
||||
@@ -186,14 +186,6 @@
|
||||
;; #{"segment" "database" "action" "indexed-entity"}
|
||||
(set/difference (set search.spec/search-models) appdb-scorer-models))
|
||||
|
||||
(defn appdb-scorers
|
||||
"The appdb-based scorers for search ranking results. Like `base-scorers`, but for scorers that need to query the appdb."
|
||||
[search-ctx]
|
||||
(when-not (search.scoring/no-scoring-required? search-ctx)
|
||||
{:bookmarked search.scoring/bookmark-score-expr
|
||||
:user-recency (search.scoring/inverse-duration
|
||||
(search.scoring/user-recency-expr search-ctx) [:now] search.config/stale-time-in-days)}))
|
||||
|
||||
(defn with-appdb-scores
|
||||
"Add appdb-based scores to `search-results` and re-sort the results based on the new combined scores.
|
||||
|
||||
@@ -208,7 +200,8 @@
|
||||
(if-not (and (seq search-results-to-score)
|
||||
(seq appdb-scorers))
|
||||
search-results
|
||||
(->> (semantic-search.db/appdb-scored-rows search-results-to-score search-ctx appdb-scorers)
|
||||
(->> (semantic-search.db/appdb-scored-rows (mapv #(select-keys % [:id :model]) search-results-to-score)
|
||||
search-ctx)
|
||||
(update-with-appdb-scores weights (keys appdb-scorers) search-results)
|
||||
(sort-by :score >)
|
||||
vec))))
|
||||
|
||||
@@ -2,45 +2,52 @@
|
||||
"Application database queries for the serialization module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for model-level helpers."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn all-users
|
||||
(mu/defn all-users
|
||||
"Every User."
|
||||
[]
|
||||
(t2/select :model/User))
|
||||
|
||||
(defn collection-id-row
|
||||
(mu/defn collection-id-row
|
||||
"The `:collection_id` row of the instance of `model` with `id`, or nil."
|
||||
[model id]
|
||||
[model :- :keyword
|
||||
id :- ms/PositiveInt]
|
||||
(t2/select-one [model :collection_id] :id id))
|
||||
|
||||
(defn existing-ids
|
||||
(mu/defn existing-ids
|
||||
"The subset of `ids` for which an instance of `model` exists."
|
||||
[model ids]
|
||||
[model :- :keyword
|
||||
ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select-pks-set model {:where [:in :id ids]}))
|
||||
|
||||
(defn instance-exists?
|
||||
(mu/defn instance-exists?
|
||||
"Whether an instance of `model` whose primary key is `id` exists."
|
||||
[model id]
|
||||
[model :- :keyword
|
||||
id :- [:maybe ms/PositiveInt]]
|
||||
(t2/exists? model (first (t2/primary-keys model)) id))
|
||||
|
||||
(defn root-collections-for-user
|
||||
(mu/defn root-collections-for-user
|
||||
"The top-level non-analytics Collections that are not personal or belong to the User with `user-id`."
|
||||
[user-id]
|
||||
[user-id :- [:maybe ::lib.schema.id/user]]
|
||||
(t2/select :model/Collection {:where [:and [:= :location "/"]
|
||||
[:or [:= :personal_owner_id nil]
|
||||
[:= :personal_owner_id user-id]]
|
||||
[:or [:= :namespace nil]
|
||||
[:!= :namespace "analytics"]]]}))
|
||||
|
||||
(defn analytics-root-collections
|
||||
(mu/defn analytics-root-collections
|
||||
"The Collections in the analytics namespace."
|
||||
[]
|
||||
(t2/select :model/Collection {:where [:= :namespace "analytics"]}))
|
||||
|
||||
(defn card-ids-in-collections
|
||||
(mu/defn card-ids-in-collections
|
||||
"The subset of `card-ids` living in the Collections with `collection-ids`."
|
||||
[card-ids collection-ids]
|
||||
[card-ids :- [:sequential ::lib.schema.id/card]
|
||||
collection-ids :- [:sequential ::lib.schema.id/collection]]
|
||||
(t2/select-pks-set :model/Card {:where [:and
|
||||
[:in :id card-ids]
|
||||
[:in :collection_id collection-ids]]}))
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
"Application database queries for the snippet-collections module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn snippet-with-collection-id
|
||||
(mu/defn snippet-with-collection-id
|
||||
"The `:collection_id` of the NativeQuerySnippet with `id`, or nil if no such NativeQuerySnippet exists. Unlike a
|
||||
bare column lookup, this distinguishes a missing snippet (nil) from one filed under the root collection
|
||||
(`{:collection_id nil}`)."
|
||||
[id]
|
||||
[id :- ::lib.schema.id/native-query-snippet]
|
||||
(t2/select-one [:model/NativeQuerySnippet :collection_id] :id id))
|
||||
|
||||
@@ -2,42 +2,50 @@
|
||||
"Application database queries for the sso module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so no other namespace in the module runs a query itself."
|
||||
(:require
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn session-user-email-and-source
|
||||
(mu/defn session-user-email-and-source
|
||||
"The email and SSO source of the User owning the Session with `session-key-hashed`, or nil."
|
||||
[session-key-hashed]
|
||||
[session-key-hashed :- :string]
|
||||
(t2/query-one {:select [:u.email :u.sso_source]
|
||||
:from [[:core_user :u]]
|
||||
:join [[:core_session :session] [:= :u.id :session.user_id]]
|
||||
:where [:= :key_hashed session-key-hashed]}))
|
||||
|
||||
(defn delete-session!
|
||||
"Delete the Session with `session-key-hashed`."
|
||||
[session-key-hashed]
|
||||
(mu/defn delete-session!
|
||||
"Delete the Session with `session-key-hashed`, returning the number deleted."
|
||||
[session-key-hashed :- :string]
|
||||
(t2/delete! :model/Session :key_hashed session-key-hashed))
|
||||
|
||||
(defn group-ids-by-name
|
||||
(mu/defn group-ids-by-name
|
||||
"The IDs of the PermissionsGroups named one of `group-names`."
|
||||
[group-names]
|
||||
[group-names :- [:set :string]]
|
||||
(t2/select-pks-set :model/PermissionsGroup :name [:in group-names]))
|
||||
|
||||
(defn insert-relay-state!
|
||||
"Insert the SsoRelayState `row`."
|
||||
[row]
|
||||
(mu/defn insert-relay-state!
|
||||
"Insert the SsoRelayState `row`, returning the number inserted."
|
||||
[row :- [:map {:closed true}
|
||||
[:id :string]
|
||||
[:continue_url [:maybe :string]]
|
||||
[:origin [:maybe :string]]
|
||||
[:embedding :boolean]
|
||||
[:expires_at ms/TemporalInstant]]]
|
||||
(t2/insert! :model/SsoRelayState row))
|
||||
|
||||
(defn unexpired-relay-state
|
||||
(mu/defn unexpired-relay-state
|
||||
"The SsoRelayState with `hashed-key` expiring after `now`, or nil."
|
||||
[hashed-key now]
|
||||
[hashed-key :- :string
|
||||
now :- ms/TemporalInstant]
|
||||
(t2/select-one :model/SsoRelayState :id hashed-key :expires_at [:> now]))
|
||||
|
||||
(defn delete-relay-state!
|
||||
"Delete the SsoRelayState with `hashed-key`."
|
||||
[hashed-key]
|
||||
(mu/defn delete-relay-state!
|
||||
"Delete the SsoRelayState with `hashed-key`, returning the number deleted."
|
||||
[hashed-key :- :string]
|
||||
(t2/delete! :model/SsoRelayState :id hashed-key))
|
||||
|
||||
(defn delete-relay-states-expired-at!
|
||||
(mu/defn delete-relay-states-expired-at!
|
||||
"Delete the SsoRelayStates expired at or before `now`, returning the number deleted."
|
||||
[now]
|
||||
[now :- ms/TemporalInstant]
|
||||
(t2/delete! :model/SsoRelayState :expires_at [:<= now]))
|
||||
|
||||
24
enterprise/backend/src/metabase_enterprise/sso/schema.clj
Normal file
24
enterprise/backend/src/metabase_enterprise/sso/schema.clj
Normal file
@@ -0,0 +1,24 @@
|
||||
(ns metabase-enterprise.sso.schema
|
||||
"Malli schemas for the sso module."
|
||||
(:require
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
(mr/def ::sso-relay-state
|
||||
"A SsoRelayState as selected from the app DB: every column of `:sso_relay_state`."
|
||||
[:map {:closed true}
|
||||
[:id :string]
|
||||
[:continue_url :string]
|
||||
[:origin [:maybe :string]]
|
||||
[:embedding :boolean]
|
||||
[:expires_at ms/TemporalInstant]
|
||||
[:created_at ms/TemporalInstant]])
|
||||
|
||||
(mr/def ::sso-relay-state.update
|
||||
"What an update (or insert) of a SsoRelayState accepts: every column of `:sso_relay_state` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:continue_url {:optional true} [:maybe :string]]
|
||||
[:origin {:optional true} [:maybe :string]]
|
||||
[:embedding {:optional true} [:maybe :boolean]]
|
||||
[:expires_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:created_at {:optional true} [:maybe ms/TemporalInstant]]])
|
||||
@@ -2,21 +2,23 @@
|
||||
"Application database queries for the stale module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for hydration."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn collection
|
||||
(mu/defn collection
|
||||
"The Collection with `collection-id`, or nil."
|
||||
[collection-id]
|
||||
[collection-id :- ::lib.schema.id/collection]
|
||||
(t2/select-one :model/Collection collection-id))
|
||||
|
||||
(defn collections-by-id
|
||||
(mu/defn collections-by-id
|
||||
"A map of ID to Collection for `collection-ids`."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:set ::lib.schema.id/collection]]
|
||||
(t2/select-pk->fn identity :model/Collection :id [:in collection-ids]))
|
||||
|
||||
(defn stale-cards
|
||||
(mu/defn stale-cards
|
||||
"The listing columns of the Cards with `card-ids`, with their latest moderation status."
|
||||
[card-ids]
|
||||
[card-ids :- [:set ::lib.schema.id/card]]
|
||||
(t2/select [:model/Card
|
||||
:id
|
||||
:dashboard_id
|
||||
@@ -47,9 +49,9 @@
|
||||
:moderated_status]]
|
||||
:id [:in card-ids]))
|
||||
|
||||
(defn stale-dashboards
|
||||
(mu/defn stale-dashboards
|
||||
"The listing columns of the Dashboards with `dashboard-ids`."
|
||||
[dashboard-ids]
|
||||
[dashboard-ids :- [:set ::lib.schema.id/dashboard]]
|
||||
(t2/select [:model/Dashboard
|
||||
:id
|
||||
:description
|
||||
@@ -71,11 +73,15 @@
|
||||
[union-queries]
|
||||
[[^:allow-subquery {:union-all union-queries} :dummy_alias]])
|
||||
|
||||
(defn stale-content-rows
|
||||
(mu/defn stale-content-rows
|
||||
"A page of `:id`/`:model` rows from the union of `union-queries`, sorted by `sort-column` (`:name` or
|
||||
`:last_used_at`) in `sort-direction`, skipping `offset` and returning up to `limit` (either may be nil for no
|
||||
restriction)."
|
||||
[union-queries sort-column sort-direction limit offset]
|
||||
[union-queries :- [:sequential :map]
|
||||
sort-column :- [:enum :name :last_used_at]
|
||||
sort-direction :- [:enum :asc :desc]
|
||||
limit :- [:maybe :int]
|
||||
offset :- [:maybe :int]]
|
||||
(t2/query (cond-> {:select [:id :model]
|
||||
:from (stale-content-union union-queries)
|
||||
:order-by [[(case sort-column
|
||||
@@ -85,8 +91,8 @@
|
||||
(some? limit) (assoc :limit limit)
|
||||
(some? offset) (assoc :offset offset))))
|
||||
|
||||
(defn stale-content-count
|
||||
(mu/defn stale-content-count
|
||||
"The total count of rows across every page [[stale-content-rows]] would return for `union-queries`."
|
||||
[union-queries]
|
||||
[union-queries :- [:sequential :map]]
|
||||
(:count (t2/query-one {:select [[:%count.* :count]]
|
||||
:from (stale-content-union union-queries)})))
|
||||
|
||||
@@ -2,19 +2,26 @@
|
||||
"Application database queries for the support-access-grants module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for model definitions, hydration methods, and transactions."
|
||||
(:require
|
||||
[malli.util :as mut]
|
||||
[metabase-enterprise.support-access-grants.schema :as support-access-grants.schema]
|
||||
[metabase.auth-identity.schema :as auth-identity.schema]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.users.schema :as users.schema]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn active-grant-exists?
|
||||
(mu/defn active-grant-exists?
|
||||
"Whether a SupportAccessGrantLog is unrevoked and ends after `now`."
|
||||
[now]
|
||||
[now :- ms/TemporalInstant]
|
||||
(t2/exists? :model/SupportAccessGrantLog :revoked_at nil :grant_end_timestamp [:> now]))
|
||||
|
||||
(defn grant
|
||||
(mu/defn grant
|
||||
"The SupportAccessGrantLog with `grant-id`, or nil."
|
||||
[grant-id]
|
||||
[grant-id :- ms/PositiveInt]
|
||||
(t2/select-one :model/SupportAccessGrantLog :id grant-id))
|
||||
|
||||
(defn current-grant
|
||||
(mu/defn current-grant
|
||||
"The newest unrevoked SupportAccessGrantLog that has not ended, or nil."
|
||||
[]
|
||||
(t2/select-one :model/SupportAccessGrantLog
|
||||
@@ -32,10 +39,14 @@
|
||||
(when (seq conditions)
|
||||
(into [:and] conditions))))
|
||||
|
||||
(defn grants-page
|
||||
(mu/defn grants-page
|
||||
"The newest-first SupportAccessGrantLogs, optionally narrowed to `ticket-number` and `user-id` and excluding revoked
|
||||
grants unless `include-revoked?`, paged by `limit` and `offset`."
|
||||
[include-revoked? ticket-number user-id limit offset]
|
||||
[include-revoked? :- [:maybe :boolean]
|
||||
ticket-number :- [:maybe :string]
|
||||
user-id :- [:maybe ::lib.schema.id/user]
|
||||
limit :- ms/PositiveInt
|
||||
offset :- ms/IntGreaterThanOrEqualToZero]
|
||||
(let [where (grants-where include-revoked? ticket-number user-id)]
|
||||
(t2/select :model/SupportAccessGrantLog
|
||||
(cond-> {:limit limit
|
||||
@@ -43,85 +54,104 @@
|
||||
:order-by [[:created_at :desc]]}
|
||||
where (assoc :where where)))))
|
||||
|
||||
(defn grant-count
|
||||
(mu/defn grant-count
|
||||
"The number of SupportAccessGrantLogs [[grants-page]] would page through."
|
||||
[include-revoked? ticket-number user-id]
|
||||
[include-revoked? :- [:maybe :boolean]
|
||||
ticket-number :- [:maybe :string]
|
||||
user-id :- [:maybe ::lib.schema.id/user]]
|
||||
(let [where (grants-where include-revoked? ticket-number user-id)]
|
||||
(t2/count :model/SupportAccessGrantLog
|
||||
(cond-> {}
|
||||
where (assoc :where where)))))
|
||||
|
||||
(defn insert-grant!
|
||||
(mu/defn insert-grant!
|
||||
"Insert `grant` and return the new instance."
|
||||
[grant]
|
||||
[grant :- (mut/select-keys ::support-access-grants.schema/support-access-grant-log.update [:user_id :ticket_number :notes :grant_start_timestamp :grant_end_timestamp])]
|
||||
(t2/insert-returning-instance! :model/SupportAccessGrantLog grant))
|
||||
|
||||
(defn update-grant!
|
||||
"Apply `changes` to the SupportAccessGrantLog with `grant-id`."
|
||||
[grant-id changes]
|
||||
(mu/defn update-grant!
|
||||
"Apply `changes` to the SupportAccessGrantLog with `grant-id`, returning the number updated."
|
||||
[grant-id :- ms/PositiveInt
|
||||
changes :- (mut/select-keys ::support-access-grants.schema/support-access-grant-log.update [:revoked_at :revoked_by_user_id])]
|
||||
(t2/update! :model/SupportAccessGrantLog grant-id changes))
|
||||
|
||||
(defn user
|
||||
(mu/defn user
|
||||
"The User with `user-id`, or nil."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one :model/User user-id))
|
||||
|
||||
(defn user-by-email
|
||||
(mu/defn user-by-email
|
||||
"The User with `email`, or nil."
|
||||
[email]
|
||||
[email :- :string]
|
||||
(t2/select-one :model/User :email email))
|
||||
|
||||
(defn user-superuser-flag-by-email
|
||||
(mu/defn user-superuser-flag-by-email
|
||||
"The `:id` and `:is_superuser` of the User with `email`, or nil."
|
||||
[email]
|
||||
[email :- :string]
|
||||
(t2/select-one [:model/User :id :is_superuser] :email email))
|
||||
|
||||
(defn user-names-and-emails
|
||||
(mu/defn user-names-and-emails
|
||||
"A map of User ID to first name and email for `user-ids`."
|
||||
[user-ids]
|
||||
[user-ids :- [:sequential ::lib.schema.id/user]]
|
||||
(t2/select-pk->fn #(select-keys % [:first_name :email]) [:model/User :id :first_name :email] :id [:in user-ids]))
|
||||
|
||||
(defn insert-user!
|
||||
(mu/defn insert-user!
|
||||
"Insert `user` and return the new instance."
|
||||
[user]
|
||||
[user :- (mut/select-keys ::users.schema/user.update [:email :first_name :last_name :is_superuser])]
|
||||
(t2/insert-returning-instance! :model/User user))
|
||||
|
||||
(defn update-user!
|
||||
"Apply `changes` to the User with `user-id`."
|
||||
[user-id changes]
|
||||
(mu/defn update-user!
|
||||
"Apply `changes` to the User with `user-id`, returning the number updated."
|
||||
[user-id :- ::lib.schema.id/user
|
||||
changes :- (mut/select-keys ::users.schema/user.update [:is_active :is_superuser])]
|
||||
(t2/update! :model/User user-id changes))
|
||||
|
||||
(defn session-exists-for-user?
|
||||
(mu/defn session-exists-for-user?
|
||||
"Whether the User with `user-id` has a Session."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/exists? :model/Session :user_id user-id))
|
||||
|
||||
(defn delete-sessions-of-user!
|
||||
"Delete the Sessions of the User with `user-id`."
|
||||
[user-id]
|
||||
(mu/defn delete-sessions-of-user!
|
||||
"Delete the Sessions of the User with `user-id`, returning the number deleted."
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/delete! :model/Session :user_id user-id))
|
||||
|
||||
(defn auth-identity-ids-of-user
|
||||
(mu/defn auth-identity-ids-of-user
|
||||
"The IDs of the AuthIdentities of the User with `user-id`."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-pks-vec :model/AuthIdentity :user_id user-id))
|
||||
|
||||
(defn support-access-auth-identity-id
|
||||
(mu/defn support-access-auth-identity-id
|
||||
"The ID of the support-access-grant AuthIdentity of the User with `user-id`, or nil."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one-pk :model/AuthIdentity :user_id user-id :provider "support-access-grant"))
|
||||
|
||||
(defn insert-auth-identity!
|
||||
"Insert the AuthIdentity `row`."
|
||||
[row]
|
||||
(mu/defn insert-auth-identity!
|
||||
"Insert the AuthIdentity `row`, returning the number inserted."
|
||||
[row :- (mut/select-keys ::auth-identity.schema/auth-identity.update [:user_id :provider :provider_id :expires_at :credentials :metadata])]
|
||||
(t2/insert! :model/AuthIdentity row))
|
||||
|
||||
(defn update-auth-identity!
|
||||
"Apply `changes` to the AuthIdentity with `auth-identity-id`."
|
||||
[auth-identity-id changes]
|
||||
(mu/defn update-auth-identity!
|
||||
"Apply `changes` to the AuthIdentity with `auth-identity-id`, returning the number updated. `changes` may be a
|
||||
whole AuthIdentity instance re-saved after a partial edit (e.g. [[metabase.auth-identity.providers.emailed-secret/mark-token-consumed]]
|
||||
round-trips the full row it was given), so every column is accepted."
|
||||
[auth-identity-id :- ms/PositiveInt
|
||||
changes :- [:map {:closed true}
|
||||
[:id {:optional true} ms/PositiveInt]
|
||||
[:user_id {:optional true} ::lib.schema.id/user]
|
||||
[:provider {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:provider_id {:optional true} [:maybe :string]]
|
||||
[:expires_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:last_used_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:confirmed_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:created_at {:optional true} ms/TemporalInstant]
|
||||
[:updated_at {:optional true} ms/TemporalInstant]
|
||||
[:credentials {:optional true} [:maybe :map]]
|
||||
[:metadata {:optional true} [:maybe :map]]]]
|
||||
(t2/update! :model/AuthIdentity auth-identity-id changes))
|
||||
|
||||
(defn expire-auth-identities!
|
||||
"Set the expiry of the AuthIdentities with `auth-identity-ids` to `expires-at`."
|
||||
[auth-identity-ids expires-at]
|
||||
(mu/defn expire-auth-identities!
|
||||
"Set the expiry of the AuthIdentities with `auth-identity-ids` to `expires-at`, returning the number updated."
|
||||
[auth-identity-ids :- [:sequential ms/PositiveInt]
|
||||
expires-at :- ms/TemporalInstant]
|
||||
(t2/update! :model/AuthIdentity :id [:in auth-identity-ids] {:expires_at expires-at}))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns metabase-enterprise.support-access-grants.schema
|
||||
"Malli schemas for support access grant API request and response bodies."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
@@ -53,3 +54,30 @@
|
||||
(mr/def ::current-grant-response
|
||||
"Schema for GET /api/ee/support-access-grants/current response."
|
||||
[:maybe ::grant-response])
|
||||
|
||||
(mr/def ::support-access-grant-log
|
||||
"A SupportAccessGrantLog as selected from the app DB: every column of `:support_access_grant_log`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:user_id [:maybe ::lib.schema.id/user]]
|
||||
[:ticket_number [:maybe :string]]
|
||||
[:notes [:maybe :string]]
|
||||
[:grant_start_timestamp ms/TemporalInstant]
|
||||
[:grant_end_timestamp ms/TemporalInstant]
|
||||
[:revoked_at [:maybe ms/TemporalInstant]]
|
||||
[:revoked_by_user_id [:maybe ::lib.schema.id/user]]
|
||||
[:created_at ms/TemporalInstant]
|
||||
[:updated_at ms/TemporalInstant]])
|
||||
|
||||
(mr/def ::support-access-grant-log.update
|
||||
"What an update (or insert) of a SupportAccessGrantLog accepts: every column of `:support_access_grant_log` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
|
||||
[:ticket_number {:optional true} [:maybe :string]]
|
||||
[:notes {:optional true} [:maybe :string]]
|
||||
[:grant_start_timestamp {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:grant_end_timestamp {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:revoked_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:revoked_by_user_id {:optional true} [:maybe ::lib.schema.id/user]]
|
||||
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]])
|
||||
|
||||
@@ -2,32 +2,40 @@
|
||||
"Application database queries for the tenants module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for model definitions, hydration methods, and transactions."
|
||||
(:require
|
||||
[malli.util :as mut]
|
||||
[metabase-enterprise.tenants.schema :as tenants.schema]
|
||||
[metabase.collections.schema :as collections.schema]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn tenant
|
||||
(mu/defn tenant
|
||||
"The Tenant with `tenant-id`, or nil."
|
||||
[tenant-id]
|
||||
[tenant-id :- ms/PositiveInt]
|
||||
(t2/select-one :model/Tenant :id tenant-id))
|
||||
|
||||
(defn tenant-by-slug
|
||||
(mu/defn tenant-by-slug
|
||||
"The Tenant with `slug`, or nil."
|
||||
[slug]
|
||||
[slug :- :string]
|
||||
(t2/select-one :model/Tenant :slug slug))
|
||||
|
||||
(defn tenant-slug
|
||||
(mu/defn tenant-slug
|
||||
"The slug of the Tenant with `tenant-id`."
|
||||
[tenant-id]
|
||||
[tenant-id :- ms/PositiveInt]
|
||||
(t2/select-one-fn :slug :model/Tenant :id tenant-id))
|
||||
|
||||
(defn tenant-collection-id
|
||||
(mu/defn tenant-collection-id
|
||||
"The root Collection ID of the Tenant with `tenant-id`."
|
||||
[tenant-id]
|
||||
[tenant-id :- ms/PositiveInt]
|
||||
(t2/select-one-fn :tenant_collection_id :model/Tenant :id tenant-id))
|
||||
|
||||
(defn tenants-page
|
||||
(mu/defn tenants-page
|
||||
"The Tenants in ID order, restricted by `status` (`\"all\"`, `\"active\"`, or `\"deactivated\"`) and paged by the
|
||||
optional `limit` and `offset`."
|
||||
[status limit offset]
|
||||
[status :- [:enum "all" "active" "deactivated"]
|
||||
limit :- [:maybe ms/PositiveInt]
|
||||
offset :- [:maybe ms/IntGreaterThanOrEqualToZero]]
|
||||
(t2/select :model/Tenant (cond-> {:order-by [[:id :asc]]
|
||||
:where (case status
|
||||
"all" [:= [:inline 1] [:inline 1]]
|
||||
@@ -35,7 +43,7 @@
|
||||
"deactivated" [:= :is_active false])}
|
||||
limit (assoc :limit limit :offset offset))))
|
||||
|
||||
(defn tenant-attributes-reducible
|
||||
(mu/defn tenant-attributes-reducible
|
||||
"Reducible attribute maps of the Tenants that have attributes."
|
||||
[]
|
||||
(t2/select-fn-reducible :attributes [:model/Tenant :attributes]
|
||||
@@ -43,41 +51,44 @@
|
||||
[:not= :attributes nil]
|
||||
[:not= :attributes "{}"]]}))
|
||||
|
||||
(defn tenant-names-and-ids-by-collection
|
||||
(mu/defn tenant-names-and-ids-by-collection
|
||||
"A map of root Collection ID to `[name id]` for the Tenants owning `collection-ids`."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:sequential ::lib.schema.id/collection]]
|
||||
(t2/select-fn->fn :tenant_collection_id (juxt :name :id) :model/Tenant :tenant_collection_id [:in collection-ids]))
|
||||
|
||||
(defn active-tenant-exists?
|
||||
(mu/defn active-tenant-exists?
|
||||
"Whether the Tenant with `tenant-id` exists and is active."
|
||||
[tenant-id]
|
||||
[tenant-id :- ms/PositiveInt]
|
||||
(t2/exists? :model/Tenant :id tenant-id :is_active true))
|
||||
|
||||
(defn tenant-name-or-slug-exists?
|
||||
(mu/defn tenant-name-or-slug-exists?
|
||||
"Whether a Tenant named `tenant-name` or with `slug` exists."
|
||||
[tenant-name slug]
|
||||
[tenant-name :- :string
|
||||
slug :- :string]
|
||||
(t2/exists? :model/Tenant {:where [:or
|
||||
[:= :slug slug]
|
||||
[:= :name tenant-name]]}))
|
||||
|
||||
(defn other-tenant-named?
|
||||
(mu/defn other-tenant-named?
|
||||
"Whether a Tenant other than `tenant-id` is named `tenant-name`."
|
||||
[tenant-name tenant-id]
|
||||
[tenant-name :- :string
|
||||
tenant-id :- ms/PositiveInt]
|
||||
(t2/exists? :model/Tenant :name tenant-name :id [:not= tenant-id]))
|
||||
|
||||
(defn insert-tenant!
|
||||
(mu/defn insert-tenant!
|
||||
"Insert `tenant` and return the new instance."
|
||||
[tenant]
|
||||
[tenant :- (mut/select-keys ::tenants.schema/tenant.update [:name :slug :attributes])]
|
||||
(t2/insert-returning-instance! :model/Tenant tenant))
|
||||
|
||||
(defn update-tenant!
|
||||
"Apply `changes` to the Tenant with `tenant-id`."
|
||||
[tenant-id changes]
|
||||
(mu/defn update-tenant!
|
||||
"Apply `changes` to the Tenant with `tenant-id`, returning the number updated."
|
||||
[tenant-id :- ms/PositiveInt
|
||||
changes :- (mut/select-keys ::tenants.schema/tenant.update [:name :attributes :is_active])]
|
||||
(t2/update! :model/Tenant {:id tenant-id} changes))
|
||||
|
||||
(defn active-member-counts
|
||||
(mu/defn active-member-counts
|
||||
"Rows of `:tenant_id` and `:count` of active personal Users for `tenant-ids`."
|
||||
[tenant-ids]
|
||||
[tenant-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/query {:select [[:tenant_id] [[:count :*] :count]]
|
||||
:from [(t2/table-name :model/User)]
|
||||
:where [:and
|
||||
@@ -86,39 +97,41 @@
|
||||
:is_active]
|
||||
:group-by [:tenant_id]}))
|
||||
|
||||
(defn deactivate-tenant-users!
|
||||
"Deactivate the active Users of the Tenant with `tenant-id`, marking them as deactivated with it."
|
||||
[tenant-id]
|
||||
(mu/defn deactivate-tenant-users!
|
||||
"Deactivate the active Users of the Tenant with `tenant-id`, marking them as deactivated with it, returning the
|
||||
number updated."
|
||||
[tenant-id :- ms/PositiveInt]
|
||||
(t2/update! :model/User {:is_active true :tenant_id tenant-id} {:is_active false :deactivated_with_tenant true}))
|
||||
|
||||
(defn reactivate-tenant-users!
|
||||
"Reactivate the Users of the Tenant with `tenant-id` that were deactivated with it."
|
||||
[tenant-id]
|
||||
(mu/defn reactivate-tenant-users!
|
||||
"Reactivate the Users of the Tenant with `tenant-id` that were deactivated with it, returning the number updated."
|
||||
[tenant-id :- ms/PositiveInt]
|
||||
(t2/update! :model/User {:is_active false :tenant_id tenant-id :deactivated_with_tenant true}
|
||||
{:is_active true :deactivated_with_tenant nil}))
|
||||
|
||||
(defn user-tenant-id
|
||||
(mu/defn user-tenant-id
|
||||
"The Tenant ID of the User with `user-id`."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one-fn :tenant_id :model/User :id user-id))
|
||||
|
||||
(defn collection-with-archived-state
|
||||
(mu/defn collection-with-archived-state
|
||||
"The Collection with `collection-id` if its archived flag is `archived?`, or nil."
|
||||
[collection-id archived?]
|
||||
[collection-id :- ::lib.schema.id/collection
|
||||
archived? :- :boolean]
|
||||
(t2/select-one :model/Collection :id collection-id :archived archived?))
|
||||
|
||||
(defn descendant-collection-ids
|
||||
(mu/defn descendant-collection-ids
|
||||
"The IDs of the Collections under the Collection with `collection-id`."
|
||||
[collection-id]
|
||||
[collection-id :- ::lib.schema.id/collection]
|
||||
(t2/select-pks-set :model/Collection :location [:like (str "/" collection-id "/%")]))
|
||||
|
||||
(defn insert-collection!
|
||||
(mu/defn insert-collection!
|
||||
"Insert `collection` and return its ID."
|
||||
[collection]
|
||||
[collection :- ::collections.schema/collection.update]
|
||||
(t2/insert-returning-pk! :model/Collection collection))
|
||||
|
||||
(defn delete-permissions-with-objects!
|
||||
"Delete the Permissions rows for `objects`."
|
||||
[objects]
|
||||
(mu/defn delete-permissions-with-objects!
|
||||
"Delete the Permissions rows for `objects`, returning the number deleted."
|
||||
[objects :- [:sequential :string]]
|
||||
(t2/query-one {:delete-from :permissions
|
||||
:where [:in :object objects]}))
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
(ns metabase-enterprise.tenants.schema
|
||||
"Malli schemas for the tenants module."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
(mr/def ::tenant.attributes
|
||||
"The `:attributes` column of a Tenant, decoded."
|
||||
[:map-of :string [:maybe [:or :string number? :boolean]]])
|
||||
|
||||
(mr/def ::tenant
|
||||
"A Tenant as selected from the app DB: every column of `:tenant`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:name :string]
|
||||
[:slug :string]
|
||||
[:is_active :boolean]
|
||||
[:updated_at ms/TemporalInstant]
|
||||
[:created_at ms/TemporalInstant]
|
||||
[:attributes [:maybe ::tenant.attributes]]
|
||||
[:tenant_collection_id ::lib.schema.id/collection]])
|
||||
|
||||
(mr/def ::tenant.update
|
||||
"What an update (or insert) of a Tenant accepts: every column of `:tenant` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:name {:optional true} [:maybe :string]]
|
||||
[:slug {:optional true} [:maybe :string]]
|
||||
[:is_active {:optional true} [:maybe :boolean]]
|
||||
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:attributes {:optional true} [:maybe ::tenant.attributes]]
|
||||
[:tenant_collection_id {:optional true} [:maybe ::lib.schema.id/collection]]])
|
||||
@@ -2,16 +2,21 @@
|
||||
"Application database queries for the transforms module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn cache-table-dependencies!
|
||||
"Store `dependencies-json` as the cached table dependencies of the Transform with `transform-id`."
|
||||
[transform-id dependencies-json]
|
||||
(mu/defn cache-table-dependencies!
|
||||
"Store `dependencies-json` as the cached table dependencies of the Transform with `transform-id`, returning the
|
||||
number updated."
|
||||
[transform-id :- ::lib.schema.id/transform
|
||||
dependencies-json :- :string]
|
||||
(t2/update! (t2/table-name :model/Transform) transform-id {:table_dependencies dependencies-json}))
|
||||
|
||||
(defn succeeded-run-counts-by-meter
|
||||
(mu/defn succeeded-run-counts-by-meter
|
||||
"Rows of `:metered_as` and `:cnt` of the TransformRuns that succeeded on `date`."
|
||||
[date]
|
||||
[date :- ms/TemporalInstant]
|
||||
(t2/query {:select [:r.metered_as [[:count :r.id] :cnt]]
|
||||
:from [[:transform_run :r]]
|
||||
:where [:and
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
"Application database queries for the transforms-inspector module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn table-source-rows
|
||||
(mu/defn table-source-rows
|
||||
"The ID, name, schema, and Database ID of the Tables with `table-ids`, as source info."
|
||||
[table-ids]
|
||||
[table-ids :- [:or [:set ::lib.schema.id/table] [:sequential ::lib.schema.id/table]]]
|
||||
(t2/select [:model/Table [:id :table-id] [:name :table-name] :schema [:db_id :db-id]] :id [:in table-ids]))
|
||||
|
||||
(defn database-engine
|
||||
(mu/defn database-engine
|
||||
"The engine of the Database with `database-id`."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select-one-fn :engine :model/Database :id database-id))
|
||||
|
||||
(defn active-fields-for-table
|
||||
(mu/defn active-fields-for-table
|
||||
"The active Fields of the Table with `table-id`."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select :model/Field :table_id table-id :active true))
|
||||
|
||||
@@ -3,58 +3,63 @@
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for model definitions and hydration methods."
|
||||
(:require
|
||||
[metabase.app-db.core :as mdb]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn table-database-ids
|
||||
(mu/defn table-database-ids
|
||||
"The set of Database IDs of the Tables with `table-ids`."
|
||||
[table-ids]
|
||||
[table-ids :- [:sequential ::lib.schema.id/table]]
|
||||
(t2/select-fn-set :db_id [:model/Table :db_id] :id [:in table-ids]))
|
||||
|
||||
(defn table-database-id
|
||||
(mu/defn table-database-id
|
||||
"The Database ID of the raw table row with `table-id`."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one-fn :db_id (t2/table-name :model/Table) :id table-id))
|
||||
|
||||
(defn database
|
||||
(mu/defn database
|
||||
"The Database with `database-id`, or nil."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select-one :model/Database database-id))
|
||||
|
||||
(defn database-engine
|
||||
(mu/defn database-engine
|
||||
"The engine of the Database with `database-id`."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select-one-fn :engine :model/Database database-id))
|
||||
|
||||
(defn update-run-message!
|
||||
(mu/defn update-run-message!
|
||||
"Set the message of the TransformRun with `run-id`."
|
||||
[run-id message]
|
||||
[run-id :- ms/PositiveInt
|
||||
message :- [:maybe :string]]
|
||||
(t2/update! :model/TransformRun :id run-id {:message message}))
|
||||
|
||||
(defn python-library
|
||||
(mu/defn python-library
|
||||
"The PythonLibrary with `library-id`, or nil."
|
||||
[library-id]
|
||||
[library-id :- ms/PositiveInt]
|
||||
(t2/select-one :model/PythonLibrary library-id))
|
||||
|
||||
(defn python-library-by-path
|
||||
(mu/defn python-library-by-path
|
||||
"The PythonLibrary at `path`, or nil."
|
||||
[path]
|
||||
[path :- :string]
|
||||
(t2/select-one :model/PythonLibrary :path path))
|
||||
|
||||
(defn upsert-python-library-source!
|
||||
(mu/defn upsert-python-library-source!
|
||||
"Insert or update the PythonLibrary at `path`, setting its source to `source`. Returns the ID of the row."
|
||||
[path source]
|
||||
[path :- :string
|
||||
source :- :string]
|
||||
(mdb/update-or-insert! :model/PythonLibrary
|
||||
{:path path}
|
||||
(constantly {:path path :source source})))
|
||||
|
||||
(defn library-sources-by-path
|
||||
(mu/defn library-sources-by-path
|
||||
"A map of path to source for every PythonLibrary."
|
||||
[]
|
||||
(t2/select-fn->fn :path :source :model/PythonLibrary))
|
||||
|
||||
(defn top-level-fields-metadata
|
||||
(mu/defn top-level-fields-metadata
|
||||
"The export metadata columns of the active top-level Fields of the Table with `table-id`, in database order."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select [:model/Field :id :name :base_type :effective_type :semantic_type :database_type :database_position]
|
||||
:table_id table-id
|
||||
:active true
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
(ns metabase-enterprise.transforms-python.schema
|
||||
"Malli schemas for the transforms-python module."
|
||||
(:require
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
(mr/def ::python-library
|
||||
"A PythonLibrary as selected from the app DB: every column of `:python_library`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:path :string]
|
||||
[:source [:maybe [:or :keyword :string]]]
|
||||
[:created_at ms/TemporalInstant]
|
||||
[:updated_at ms/TemporalInstant]
|
||||
[:entity_id :string]])
|
||||
|
||||
(mr/def ::python-library.update
|
||||
"What an update (or insert) of a PythonLibrary accepts: every column of `:python_library` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:path {:optional true} [:maybe :string]]
|
||||
[:source {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:entity_id {:optional true} [:maybe :string]]])
|
||||
@@ -2,24 +2,26 @@
|
||||
"Application database queries for the upload-management module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn attached-dwh-database-id
|
||||
(mu/defn attached-dwh-database-id
|
||||
"The ID of the attached data warehouse Database, or nil."
|
||||
[]
|
||||
(t2/select-one-fn :id :model/Database :is_attached_dwh true))
|
||||
|
||||
(defn non-upload-tables-for-database
|
||||
(mu/defn non-upload-tables-for-database
|
||||
"The active Tables of the Database with `database-id` that were not uploaded."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select :model/Table :db_id database-id :active true :is_upload false))
|
||||
|
||||
(defn upload-tables
|
||||
(mu/defn upload-tables
|
||||
"The active uploaded Tables, ordered by name."
|
||||
[]
|
||||
(t2/select :model/Table :active true :is_upload true {:order-by [[:name :asc]]}))
|
||||
|
||||
(defn table
|
||||
(mu/defn table
|
||||
"The Table with `table-id`, or nil."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one :model/Table table-id))
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
(:require
|
||||
[clojure.test :refer :all]
|
||||
[metabase-enterprise.semantic-search.test-util :as semantic.tu]
|
||||
[metabase.metabot.db :as metabot.db]
|
||||
[metabase.metabot.tools.search :as search]
|
||||
[metabase.permissions.core :as perms]
|
||||
[metabase.search.core :as search-core]
|
||||
[metabase.search.engine :as search.engine]
|
||||
[metabase.search.ingestion :as search.ingestion]
|
||||
[metabase.search.test-util :as search.tu]
|
||||
[metabase.test :as mt]
|
||||
[toucan2.core :as t2]))
|
||||
[metabase.test :as mt]))
|
||||
|
||||
(use-fixtures :once #'semantic.tu/once-fixture)
|
||||
|
||||
@@ -109,9 +109,9 @@
|
||||
(testing "search with metabot verified-or-curated content flag"
|
||||
(let [metabot {:entity_id "test-bot"
|
||||
:use_verified_content true}]
|
||||
(with-redefs [t2/select-one (fn [model & _]
|
||||
(is (= :model/Metabot model) "Should query for Metabot model")
|
||||
metabot)
|
||||
(with-redefs [metabot.db/metabot-by-entity-id (fn [entity-id]
|
||||
(is (= "test-bot" entity-id) "Should look up the Metabot")
|
||||
metabot)
|
||||
search-core/search (fn [context]
|
||||
;; use_verified_content now drives the curated filter, not :verified
|
||||
(is (true? (:curated? context)))
|
||||
|
||||
@@ -377,7 +377,8 @@
|
||||
(is (some #(str/includes? % "collection") (keys files-after-export)))
|
||||
(is (some #(str/includes? % "card") (keys files-after-export))))
|
||||
(t2/delete! :model/RemoteSyncTask :id (:id export-task))
|
||||
(let [import-task (t2/with-connection [_conn (app-db/app-db) (t2/insert-returning-instance! :model/RemoteSyncTask {:sync_task_type "import" :initiated_by (mt/user->id :rasta)})])
|
||||
(let [import-task (t2/with-connection [_conn (app-db/app-db)]
|
||||
(t2/insert-returning-instance! :model/RemoteSyncTask {:sync_task_type "import" :initiated_by (mt/user->id :rasta)}))
|
||||
import-result (impl/import! (source.p/snapshot mock-main) (:id import-task))]
|
||||
(remote-sync.task/complete-sync-task! (:id import-task))
|
||||
(is (= :success (:status import-result)))
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
[metabase.analytics-interface.core :as analytics]
|
||||
[metabase.app-db.core :as mdb]
|
||||
[metabase.search.appdb.core :as appdb]
|
||||
[metabase.search.db :as search.db]
|
||||
[metabase.search.engine :as search.engine]
|
||||
[metabase.search.in-place.legacy :as in-place.legacy]
|
||||
[metabase.search.in-place.scoring :as in-place.scoring]
|
||||
[metabase.search.settings :as search.settings]
|
||||
[metabase.test :as mt])
|
||||
@@ -131,7 +131,7 @@
|
||||
(fn [result _]
|
||||
{:result (dissoc result :score)
|
||||
:score (:score result)})
|
||||
in-place.legacy/results
|
||||
search.db/in-place-search-reducible
|
||||
(fn [_]
|
||||
(reset! legacy-called? true)
|
||||
(reify clojure.lang.IReduceInit
|
||||
|
||||
@@ -91,8 +91,8 @@
|
||||
analytics
|
||||
analytics-interface
|
||||
api
|
||||
api-scope
|
||||
api-keys
|
||||
api-scope
|
||||
appearance
|
||||
audit-app
|
||||
auth-identity
|
||||
@@ -120,16 +120,19 @@
|
||||
explorations
|
||||
formatter
|
||||
geojson
|
||||
indexed-entities
|
||||
initialization-status
|
||||
interestingness
|
||||
internal-stats
|
||||
llm
|
||||
login-history
|
||||
mcp
|
||||
measures
|
||||
metabot
|
||||
mq
|
||||
notification
|
||||
oauth-server
|
||||
osi
|
||||
permissions
|
||||
premium-features
|
||||
public-sharing
|
||||
@@ -139,6 +142,7 @@
|
||||
sample-data
|
||||
search
|
||||
secrets
|
||||
segments
|
||||
server
|
||||
session
|
||||
settings
|
||||
@@ -158,7 +162,8 @@
|
||||
util
|
||||
version
|
||||
view-log
|
||||
warehouse-schema})
|
||||
warehouse-schema
|
||||
xrays})
|
||||
|
||||
(defn- affected-modules
|
||||
"Set of modules that are direct or indirect dependents of `modules`, and thus are affected by changes to them.
|
||||
|
||||
@@ -2,24 +2,29 @@
|
||||
"Application database queries for the bigquery-cloud-sdk driver. Every function here is a direct Toucan 2 call with
|
||||
no additional logic, so the rest of the driver never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.warehouses.schema :as warehouses.schema]
|
||||
;; the driver persists dataset-filter and project-id migrations of its Database details back to the app DB
|
||||
^{:clj-kondo/ignore [:discouraged-namespace]}
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn database
|
||||
(mu/defn database
|
||||
"The Database with `database-id`, or nil."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select-one :model/Database database-id))
|
||||
|
||||
(defn update-database-details!
|
||||
"Set the details of the Database with `database-id`."
|
||||
[database-id details]
|
||||
(mu/defn update-database-details!
|
||||
"Set the details of the Database with `database-id`, returning the number updated."
|
||||
[database-id :- ::lib.schema.id/database
|
||||
details :- ::warehouses.schema/database.details]
|
||||
(t2/update! :model/Database database-id {:details details}))
|
||||
|
||||
(defn set-table-schemas!
|
||||
(mu/defn set-table-schemas!
|
||||
"Set the schema of the Tables of the Database with `database-id` that do not have `schema` yet, overwriting any
|
||||
stale schema they already have."
|
||||
[database-id schema]
|
||||
stale schema they already have, returning the number updated."
|
||||
[database-id :- ::lib.schema.id/database
|
||||
schema :- :string]
|
||||
(t2/query-one {:update (t2/table-name :model/Table)
|
||||
:set {:schema schema}
|
||||
:where [:and
|
||||
@@ -28,7 +33,8 @@
|
||||
[:= :schema nil]
|
||||
[:not= :schema schema]]]}))
|
||||
|
||||
(defn active-partitioned-field-exists?
|
||||
(mu/defn active-partitioned-field-exists?
|
||||
"Whether the Table with `table-id` has an active database-partitioned Field named `field-name`."
|
||||
[table-id field-name]
|
||||
[table-id :- ::lib.schema.id/table
|
||||
field-name :- :string]
|
||||
(t2/exists? :model/Field :table_id table-id :name field-name :database_partitioned true :active true))
|
||||
|
||||
@@ -2,232 +2,241 @@
|
||||
"Application database queries for the actions module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so no other namespace in the module runs a query itself (model definitions still use `toucan2.core`)."
|
||||
(:require
|
||||
[metabase.actions.schema :as actions.schema]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn database-for-action
|
||||
(mu/defn database-for-action
|
||||
"The Database of the model Card of the Action with `action-id`, or nil."
|
||||
[action-id]
|
||||
[action-id :- ::lib.schema.id/action]
|
||||
(t2/select-one :model/Database {:select [:db.*]
|
||||
:from :action
|
||||
:join [[:report_card :card] [:= :card.id :action.model_id]
|
||||
[:metabase_database :db] [:= :db.id :card.database_id]]
|
||||
:where [:= :action.id action-id]}))
|
||||
|
||||
(defn table-database-id
|
||||
(mu/defn table-database-id
|
||||
"The Database id of the Table with `table-id`, or nil."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one-fn :db_id [:model/Table :db_id] table-id))
|
||||
|
||||
(defn card-query
|
||||
(mu/defn card-query
|
||||
"The query of the Card with `card-id`, or nil."
|
||||
[card-id]
|
||||
[card-id :- ::lib.schema.id/card]
|
||||
(t2/select-one-fn :dataset_query :model/Card :id card-id))
|
||||
|
||||
(defn card
|
||||
(mu/defn card
|
||||
"The Card with `card-id`, or nil."
|
||||
[card-id]
|
||||
[card-id :- ::lib.schema.id/card]
|
||||
(t2/select-one :model/Card :id card-id))
|
||||
|
||||
(defn cards
|
||||
(mu/defn cards
|
||||
"The Cards with `card-ids`."
|
||||
[card-ids]
|
||||
[card-ids :- [:set ::lib.schema.id/card]]
|
||||
(t2/select :model/Card :id [:in card-ids]))
|
||||
|
||||
(defn cards-by-id
|
||||
(mu/defn cards-by-id
|
||||
"A map of Card id to Card for the Cards with `card-ids`."
|
||||
[card-ids]
|
||||
[card-ids :- [:sequential ::lib.schema.id/card]]
|
||||
(t2/select-pk->fn identity :model/Card :id [:in card-ids]))
|
||||
|
||||
(defn card-type
|
||||
(mu/defn card-type
|
||||
"The type of the Card with `card-id`, or nil."
|
||||
[card-id]
|
||||
[card-id :- ::lib.schema.id/card]
|
||||
(t2/select-one-fn :type [:model/Card :type :card_schema] :id card-id))
|
||||
|
||||
(defn table
|
||||
(mu/defn table
|
||||
"The Table with `table-id`, or nil."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one :model/Table :id table-id))
|
||||
|
||||
(defn tables
|
||||
(mu/defn tables
|
||||
"The Tables with `table-ids`."
|
||||
[table-ids]
|
||||
[table-ids :- [:sequential ::lib.schema.id/table]]
|
||||
(t2/select :model/Table :id [:in table-ids]))
|
||||
|
||||
(defn database
|
||||
(mu/defn database
|
||||
"The Database with `database-id`, or nil."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select-one :model/Database :id database-id))
|
||||
|
||||
(defn dashcard-in-dashboard
|
||||
(mu/defn dashcard-in-dashboard
|
||||
"The DashboardCard with `dashcard-id` on the Dashboard with `dashboard-id`, or nil."
|
||||
[dashcard-id dashboard-id]
|
||||
[dashcard-id :- ::lib.schema.id/dashcard
|
||||
dashboard-id :- ::lib.schema.id/dashboard]
|
||||
(t2/select-one :model/DashboardCard :id dashcard-id :dashboard_id dashboard-id))
|
||||
|
||||
(defn dashcard-dashboard-id
|
||||
(mu/defn dashcard-dashboard-id
|
||||
"The `:dashboard_id` of the DashboardCard with `dashcard-id`, or nil."
|
||||
[dashcard-id]
|
||||
[dashcard-id :- ::lib.schema.id/dashcard]
|
||||
(t2/select-one-fn :dashboard_id [:model/DashboardCard :dashboard_id] dashcard-id))
|
||||
|
||||
(defn dashcard-action-id
|
||||
(mu/defn dashcard-action-id
|
||||
"The Action id of the DashboardCard with `dashcard-id`, or nil."
|
||||
[dashcard-id]
|
||||
[dashcard-id :- ::lib.schema.id/dashcard]
|
||||
(t2/select-one-fn :action_id :model/DashboardCard :id dashcard-id))
|
||||
|
||||
(defn delete-dashcards-for-action!
|
||||
"Delete the DashboardCards of the Action with `action-id`."
|
||||
[action-id]
|
||||
(mu/defn delete-dashcards-for-action!
|
||||
"Delete the DashboardCards of the Action with `action-id`, returning the number deleted."
|
||||
[action-id :- ::lib.schema.id/action]
|
||||
(t2/delete! :model/DashboardCard :action_id action-id))
|
||||
|
||||
(defn insert-action!
|
||||
(mu/defn insert-action!
|
||||
"Insert the Action `row` and return the inserted instance."
|
||||
[row]
|
||||
[row :- ::actions.schema/action.for-insert]
|
||||
(t2/insert-returning-instance! :model/Action row))
|
||||
|
||||
(defn update-action!
|
||||
"Apply `changes` to the Action with `action-id`."
|
||||
[action-id changes]
|
||||
(mu/defn update-action!
|
||||
"Apply `changes` to the Action with `action-id`, returning the number updated."
|
||||
[action-id :- ::lib.schema.id/action
|
||||
changes :- ::actions.schema/action.for-update]
|
||||
(t2/update! :model/Action action-id changes))
|
||||
|
||||
(defn insert-query-action!
|
||||
"Insert the QueryAction `action`."
|
||||
[action]
|
||||
(mu/defn insert-query-action!
|
||||
"Insert the QueryAction `action`, returning the number inserted."
|
||||
[action :- ::actions.schema/query-action.update]
|
||||
(t2/insert! :model/QueryAction action))
|
||||
|
||||
(defn insert-http-action!
|
||||
"Insert the HTTPAction `action`."
|
||||
[action]
|
||||
(mu/defn insert-http-action!
|
||||
"Insert the HTTPAction `action`, returning the number inserted."
|
||||
[action :- ::actions.schema/httpaction.update]
|
||||
(t2/insert! :model/HTTPAction action))
|
||||
|
||||
(defn insert-implicit-action!
|
||||
"Insert the ImplicitAction `action`."
|
||||
[action]
|
||||
(mu/defn insert-implicit-action!
|
||||
"Insert the ImplicitAction `action`, returning the number inserted."
|
||||
[action :- ::actions.schema/implicit-action.update]
|
||||
(t2/insert! :model/ImplicitAction action))
|
||||
|
||||
(defn update-query-action!
|
||||
"Apply `changes` to the QueryAction with `action-id`."
|
||||
[action-id changes]
|
||||
(mu/defn update-query-action!
|
||||
"Apply `changes` to the QueryAction with `action-id`, returning the number updated."
|
||||
[action-id :- ::lib.schema.id/action
|
||||
changes :- ::actions.schema/query-action.update]
|
||||
(t2/update! :model/QueryAction action-id changes))
|
||||
|
||||
(defn update-http-action!
|
||||
"Apply `changes` to the HTTPAction with `action-id`."
|
||||
[action-id changes]
|
||||
(mu/defn update-http-action!
|
||||
"Apply `changes` to the HTTPAction with `action-id`, returning the number updated."
|
||||
[action-id :- ::lib.schema.id/action
|
||||
changes :- ::actions.schema/httpaction.update]
|
||||
(t2/update! :model/HTTPAction action-id changes))
|
||||
|
||||
(defn update-implicit-action!
|
||||
"Apply `changes` to the ImplicitAction with `action-id`."
|
||||
[action-id changes]
|
||||
(mu/defn update-implicit-action!
|
||||
"Apply `changes` to the ImplicitAction with `action-id`, returning the number updated."
|
||||
[action-id :- ::lib.schema.id/action
|
||||
changes :- ::actions.schema/implicit-action.update]
|
||||
(t2/update! :model/ImplicitAction action-id changes))
|
||||
|
||||
(defn delete-query-action!
|
||||
"Delete the QueryAction of the Action with `action-id`."
|
||||
[action-id]
|
||||
(mu/defn delete-query-action!
|
||||
"Delete the QueryAction of the Action with `action-id`, returning the number deleted."
|
||||
[action-id :- ::lib.schema.id/action]
|
||||
(t2/delete! :model/QueryAction :action_id action-id))
|
||||
|
||||
(defn delete-http-action!
|
||||
"Delete the HTTPAction of the Action with `action-id`."
|
||||
[action-id]
|
||||
(mu/defn delete-http-action!
|
||||
"Delete the HTTPAction of the Action with `action-id`, returning the number deleted."
|
||||
[action-id :- ::lib.schema.id/action]
|
||||
(t2/delete! :model/HTTPAction :action_id action-id))
|
||||
|
||||
(defn delete-implicit-action!
|
||||
"Delete the ImplicitAction of the Action with `action-id`."
|
||||
[action-id]
|
||||
(mu/defn delete-implicit-action!
|
||||
"Delete the ImplicitAction of the Action with `action-id`, returning the number deleted."
|
||||
[action-id :- ::lib.schema.id/action]
|
||||
(t2/delete! :model/ImplicitAction :action_id action-id))
|
||||
|
||||
(defn query-actions
|
||||
(mu/defn query-actions
|
||||
"The QueryActions of the Actions with `action-ids`."
|
||||
[action-ids]
|
||||
[action-ids :- [:sequential ::lib.schema.id/action]]
|
||||
(t2/select :model/QueryAction :action_id [:in action-ids]))
|
||||
|
||||
(defn query-action
|
||||
(mu/defn query-action
|
||||
"The QueryAction of the Action with `action-id`, or nil."
|
||||
[action-id]
|
||||
[action-id :- ::lib.schema.id/action]
|
||||
(t2/select-one :model/QueryAction :action_id action-id))
|
||||
|
||||
(defn http-actions
|
||||
(mu/defn http-actions
|
||||
"The HTTPActions of the Actions with `action-ids`."
|
||||
[action-ids]
|
||||
[action-ids :- [:sequential ::lib.schema.id/action]]
|
||||
(t2/select :model/HTTPAction :action_id [:in action-ids]))
|
||||
|
||||
(defn implicit-actions
|
||||
(mu/defn implicit-actions
|
||||
"The ImplicitActions of the Actions with `action-ids`."
|
||||
[action-ids]
|
||||
[action-ids :- [:sequential ::lib.schema.id/action]]
|
||||
(t2/select :model/ImplicitAction :action_id [:in action-ids]))
|
||||
|
||||
(defn actions-with-id
|
||||
(mu/defn actions-with-id
|
||||
"The Actions with `action-id` (usually a single Action, since ids are unique)."
|
||||
[action-id]
|
||||
[action-id :- [:maybe ::lib.schema.id/action]]
|
||||
(t2/select :model/Action :id action-id))
|
||||
|
||||
(defn actions-with-ids
|
||||
(mu/defn actions-with-ids
|
||||
"The Actions whose `:id` is in `action-ids`."
|
||||
[action-ids]
|
||||
[action-ids :- [:sequential ::lib.schema.id/action]]
|
||||
(t2/select :model/Action :id [:in action-ids]))
|
||||
|
||||
(defn unarchived-action-with-id
|
||||
(mu/defn unarchived-action-with-id
|
||||
"The unarchived Actions with `action-id` (usually a single Action, since ids are unique)."
|
||||
[action-id]
|
||||
[action-id :- [:maybe ::lib.schema.id/action]]
|
||||
(t2/select :model/Action :id action-id :archived false))
|
||||
|
||||
(defn action-with-entity-id
|
||||
(mu/defn action-with-entity-id
|
||||
"The Actions with `entity-id` (usually a single Action, since entity ids are unique)."
|
||||
[entity-id]
|
||||
[entity-id :- :string]
|
||||
(t2/select :model/Action :entity_id entity-id))
|
||||
|
||||
(defn actions-of-type
|
||||
(mu/defn actions-of-type
|
||||
"The Actions of `action-type`."
|
||||
[action-type]
|
||||
[action-type :- :keyword]
|
||||
(t2/select :model/Action :type action-type))
|
||||
|
||||
(defn unarchived-actions-for-models
|
||||
(mu/defn unarchived-actions-for-models
|
||||
"The unarchived Actions whose `:model_id` is in `model-ids`."
|
||||
[model-ids]
|
||||
[model-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select :model/Action :model_id [:in model-ids] :archived false))
|
||||
|
||||
(defn unarchived-non-http-actions-for-model
|
||||
(mu/defn unarchived-non-http-actions-for-model
|
||||
"The unarchived, non-HTTP Actions of the model Card with `model-id`."
|
||||
[model-id]
|
||||
[model-id :- ms/PositiveInt]
|
||||
(t2/select :model/Action :model_id model-id :archived false :type [:not= "http"]))
|
||||
|
||||
(defn unarchived-non-http-actions-for-models
|
||||
(mu/defn unarchived-non-http-actions-for-models
|
||||
"The unarchived, non-HTTP Actions whose `:model_id` is in `model-ids`."
|
||||
[model-ids]
|
||||
[model-ids :- [:set ms/PositiveInt]]
|
||||
(t2/select :model/Action :model_id [:in model-ids] :archived false :type [:not= "http"]))
|
||||
|
||||
(defn fields-for-parameters
|
||||
(mu/defn fields-for-parameters
|
||||
"The id, base type, display name, and description of the Fields with `field-ids`."
|
||||
[field-ids]
|
||||
[field-ids :- [:set ::lib.schema.id/field]]
|
||||
(t2/select [:model/Field :id :base_type :display_name :description] :id [:in field-ids]))
|
||||
|
||||
(defn action-database-settings
|
||||
(mu/defn action-database-settings
|
||||
"The id and Database settings of the Actions with `action-ids`."
|
||||
[action-ids]
|
||||
[action-ids :- [:sequential ::lib.schema.id/action]]
|
||||
(t2/query {:select [:action.id :db.settings]
|
||||
:from :action
|
||||
:join [[:report_card :card] [:= :card.id :action.model_id]
|
||||
[:metabase_database :db] [:= :db.id :card.database_id]]
|
||||
:where [:in :action.id action-ids]}))
|
||||
|
||||
(defn card-scope-columns
|
||||
(mu/defn card-scope-columns
|
||||
"The query, Collection id, Database id, and display of the Card with `card-id`, or nil."
|
||||
[card-id]
|
||||
[card-id :- ::lib.schema.id/card]
|
||||
(t2/select-one [:model/Card :dataset_query :collection_id :database_id :display] card-id))
|
||||
|
||||
(defn dashboard-collection-id
|
||||
(mu/defn dashboard-collection-id
|
||||
"The Collection id of the Dashboard with `dashboard-id`, or nil."
|
||||
[dashboard-id]
|
||||
[dashboard-id :- ::lib.schema.id/dashboard]
|
||||
(t2/select-one-fn :collection_id [:model/Dashboard :collection_id] dashboard-id))
|
||||
|
||||
(defn destination-database-exists-for-router?
|
||||
(mu/defn destination-database-exists-for-router?
|
||||
"Whether the Database with `database-id` has routing destinations."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/exists? :model/Database :router_database_id database-id))
|
||||
|
||||
(defn writable-table-exists?
|
||||
(mu/defn writable-table-exists?
|
||||
"Whether the Database with `database-id` has a writable Table."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/exists? :model/Table :db_id database-id :is_writable true))
|
||||
|
||||
(defn table-with-unknown-writability-exists?
|
||||
(mu/defn table-with-unknown-writability-exists?
|
||||
"Whether the Database with `database-id` has a Table whose writability is unknown."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/exists? :model/Table :db_id database-id :is_writable nil))
|
||||
|
||||
@@ -177,3 +177,51 @@
|
||||
(mr/def ::action.for-update
|
||||
"Schema for updating an Action (REST API or internally)."
|
||||
(action-schema :update))
|
||||
|
||||
(mr/def ::httpaction
|
||||
"A HTTPAction as selected from the app DB: every column of `:http_action`."
|
||||
[:map {:closed true}
|
||||
[:action_id ::lib.schema.id/action]
|
||||
[:template ::http-action.template]
|
||||
[:response_handle [:maybe :string]]
|
||||
[:error_handle [:maybe :string]]])
|
||||
|
||||
(mr/def ::httpaction.update
|
||||
"What an update (or insert) of a HTTPAction accepts: every column of `:http_action` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:action_id {:optional true} [:maybe ::lib.schema.id/action]]
|
||||
[:template {:optional true} [:maybe ::http-action.template]]
|
||||
[:response_handle {:optional true} [:maybe :string]]
|
||||
[:error_handle {:optional true} [:maybe :string]]])
|
||||
|
||||
(mr/def ::implicit-action.row
|
||||
"A ImplicitAction as selected from the app DB: every column of `:implicit_action`."
|
||||
[:map {:closed true}
|
||||
[:action_id ::lib.schema.id/action]
|
||||
[:kind [:or :keyword :string]]])
|
||||
|
||||
(mr/def ::implicit-action.update
|
||||
"What an update (or insert) of a ImplicitAction accepts: every column of `:implicit_action` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:action_id {:optional true} [:maybe ::lib.schema.id/action]]
|
||||
[:kind {:optional true} [:maybe [:or :keyword :string]]]])
|
||||
|
||||
(mr/def ::query-action.dataset-query
|
||||
"The `:dataset_query` column of a QueryAction, decoded."
|
||||
:map)
|
||||
|
||||
(mr/def ::query-action.row
|
||||
"A QueryAction as selected from the app DB: every column of `:query_action`."
|
||||
[:map {:closed true}
|
||||
[:action_id ::lib.schema.id/action]
|
||||
[:database_id ::lib.schema.id/database]
|
||||
[:dataset_query ::query-action.dataset-query]
|
||||
[:legacy_query [:maybe :string]]])
|
||||
|
||||
(mr/def ::query-action.update
|
||||
"What an update (or insert) of a QueryAction accepts: every column of `:query_action` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:action_id {:optional true} [:maybe ::lib.schema.id/action]]
|
||||
[:database_id {:optional true} [:maybe ::lib.schema.id/database]]
|
||||
[:dataset_query {:optional true} [:maybe ::query-action.dataset-query]]
|
||||
[:legacy_query {:optional true} [:maybe :string]]])
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for hydration."
|
||||
(:require
|
||||
[metabase.collections.models.collection :as collection]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn unarchived-models-visible-to-user
|
||||
(mu/defn unarchived-models-visible-to-user
|
||||
"The unarchived model Cards in Collections the current user can read."
|
||||
[]
|
||||
(t2/select :model/Card {:where [:and
|
||||
@@ -13,22 +15,24 @@
|
||||
[:= :archived false]
|
||||
(collection/visible-collection-filter-clause)]}))
|
||||
|
||||
(defn public-actions
|
||||
(mu/defn public-actions
|
||||
"The name, id, public uuid, and model id of the unarchived Actions that are publicly shared."
|
||||
[]
|
||||
(t2/select [:model/Action :name :id :public_uuid :model_id], :public_uuid [:not= nil], :archived false))
|
||||
|
||||
(defn delete-action!
|
||||
(mu/defn delete-action!
|
||||
"Delete the Action with `action-id`."
|
||||
[action-id]
|
||||
[action-id :- ::lib.schema.id/action]
|
||||
(t2/delete! :model/Action :id action-id))
|
||||
|
||||
(defn database
|
||||
(mu/defn database
|
||||
"The Database with `database-id`, or nil."
|
||||
[database-id]
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select-one :model/Database :id database-id))
|
||||
|
||||
(defn set-action-public-uuid!
|
||||
(mu/defn set-action-public-uuid!
|
||||
"Set the public uuid of the Action with `action-id` and the User who made it public."
|
||||
[action-id public-uuid made-public-by-id]
|
||||
[action-id :- ::lib.schema.id/action
|
||||
public-uuid :- [:maybe :string]
|
||||
made-public-by-id :- [:maybe ::lib.schema.id/user]]
|
||||
(t2/update! :model/Action action-id {:public_uuid public-uuid, :made_public_by_id made-public-by-id}))
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
"Application database queries for the activity feed module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so no other namespace in the module runs a query itself (model definitions still use `toucan2.core`)."
|
||||
(:require
|
||||
[metabase.activity-feed.schema :as activity-feed.schema]
|
||||
[metabase.collections.models.collection :as collection]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.honey-sql-2 :as h2x]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn recent-cards
|
||||
(mu/defn recent-cards
|
||||
"The recently viewed Cards with `ids`, with their Collection and Dashboard names."
|
||||
[ids]
|
||||
[ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select [:model/Card
|
||||
:id :name :collection_id :description :display
|
||||
:dataset_query :type :archived :card_schema
|
||||
@@ -18,9 +22,9 @@
|
||||
:left-join [:collection [:= :collection.id :report_card.collection_id]
|
||||
[:report_dashboard :dashboard] [:= :dashboard.id :report_card.dashboard_id]]}))
|
||||
|
||||
(defn recent-dashboards
|
||||
(mu/defn recent-dashboards
|
||||
"The recently viewed Dashboards with `ids`, with their Collection names."
|
||||
[ids]
|
||||
[ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select [:model/Dashboard
|
||||
:id :name :collection_id :description
|
||||
:archived
|
||||
@@ -28,9 +32,9 @@
|
||||
{:where [:in :report_dashboard.id ids]
|
||||
:left-join [:collection [:= :collection.id :report_dashboard.collection_id]]}))
|
||||
|
||||
(defn recent-tables
|
||||
(mu/defn recent-tables
|
||||
"The recently viewed Tables with `ids`, with their Database names and sync status."
|
||||
[ids]
|
||||
[ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select [:model/Table
|
||||
:id :name :db_id :active
|
||||
:display_name [:metabase_database.initial_sync_status :initial-sync-status]
|
||||
@@ -39,10 +43,10 @@
|
||||
{:where [:in :metabase_table.id ids]
|
||||
:left-join [:metabase_database [:= :metabase_database.id :metabase_table.db_id]]}))
|
||||
|
||||
(defn recent-dashboard-and-table-views
|
||||
(mu/defn recent-dashboard-and-table-views
|
||||
"Up to `limit` most recently viewed unarchived, active Dashboards and Tables with their view counts and last
|
||||
viewer."
|
||||
[limit]
|
||||
[limit :- ms/PositiveInt]
|
||||
(t2/select [:model/RecentViews
|
||||
[[:min :recent_views.user_id] :user_id]
|
||||
:model
|
||||
@@ -66,9 +70,9 @@
|
||||
[:= :model "table"]
|
||||
[:= :t.id :model_id]]]}))
|
||||
|
||||
(defn recent-card-runs
|
||||
(mu/defn recent-card-runs
|
||||
"Up to `limit` most recently run question Cards with their run counts and last runner."
|
||||
[limit]
|
||||
[limit :- ms/PositiveInt]
|
||||
(t2/select [:model/QueryExecution
|
||||
[:%min.executor_id :user_id]
|
||||
[:query_execution.card_id :model_id]
|
||||
@@ -80,70 +84,75 @@
|
||||
:order-by [[:max_ts :desc]]
|
||||
:limit limit}))
|
||||
|
||||
(defn card-exists?
|
||||
(mu/defn card-exists?
|
||||
"Whether a Card with `id` exists."
|
||||
[id]
|
||||
[id :- ::lib.schema.id/card]
|
||||
(t2/exists? :model/Card :id id))
|
||||
|
||||
(defn dashboard-exists?
|
||||
(mu/defn dashboard-exists?
|
||||
"Whether a Dashboard with `id` exists."
|
||||
[id]
|
||||
[id :- ::lib.schema.id/dashboard]
|
||||
(t2/exists? :model/Dashboard :id id))
|
||||
|
||||
(defn table-exists?
|
||||
(mu/defn table-exists?
|
||||
"Whether a Table with `id` exists."
|
||||
[id]
|
||||
[id :- ::lib.schema.id/table]
|
||||
(t2/exists? :model/Table :id id))
|
||||
|
||||
(defn collection-exists?
|
||||
(mu/defn collection-exists?
|
||||
"Whether a Collection with `id` exists."
|
||||
[id]
|
||||
[id :- ::lib.schema.id/collection]
|
||||
(t2/exists? :model/Collection :id id))
|
||||
|
||||
(defn document-exists?
|
||||
(mu/defn document-exists?
|
||||
"Whether a Document with `id` exists."
|
||||
[id]
|
||||
[id :- ms/PositiveInt]
|
||||
(t2/exists? :model/Document :id id))
|
||||
|
||||
(defn card
|
||||
(mu/defn card
|
||||
"The Card with `id`, or nil."
|
||||
[id]
|
||||
[id :- ::lib.schema.id/card]
|
||||
(t2/select-one :model/Card :id id))
|
||||
|
||||
(defn table
|
||||
(mu/defn table
|
||||
"The Table with `id`, or nil."
|
||||
[id]
|
||||
[id :- ::lib.schema.id/table]
|
||||
(t2/select-one :model/Table :id id))
|
||||
|
||||
(defn collection
|
||||
(mu/defn collection
|
||||
"The Collection with `id`, or nil."
|
||||
[id]
|
||||
[id :- ::lib.schema.id/collection]
|
||||
(t2/select-one :model/Collection :id id))
|
||||
|
||||
(defn document
|
||||
(mu/defn document
|
||||
"The Document with `id`, or nil."
|
||||
[id]
|
||||
[id :- ms/PositiveInt]
|
||||
(t2/select-one :model/Document :id id))
|
||||
|
||||
(defn dashboard
|
||||
(mu/defn dashboard
|
||||
"The Dashboard with `dashboard-id`, or nil."
|
||||
[dashboard-id]
|
||||
[dashboard-id :- ::lib.schema.id/dashboard]
|
||||
(t2/select-one :model/Dashboard :id dashboard-id))
|
||||
|
||||
(defn card-document-id
|
||||
(mu/defn card-document-id
|
||||
"The Document id of the Card with `card-id`, or nil."
|
||||
[card-id]
|
||||
[card-id :- ::lib.schema.id/card]
|
||||
(t2/select-one-fn :document_id :model/Card :id card-id))
|
||||
|
||||
(defn recent-views-for-user-context
|
||||
(mu/defn recent-views-for-user-context
|
||||
"The RecentViews of the User with `user-id` in `context`, newest first."
|
||||
[user-id context]
|
||||
[user-id :- ::lib.schema.id/user
|
||||
context :- [:or :keyword :string]]
|
||||
(t2/select :model/RecentViews :user_id user-id :context context {:order-by [[:timestamp :desc]]}))
|
||||
|
||||
(defn recent-view-ids-to-prune
|
||||
(mu/defn recent-view-ids-to-prune
|
||||
"The ids of the RecentViews of the User with `user-id` for `db-model` in `context` beyond the newest `keep` of
|
||||
them, restricted to the Cards of `card-type` when non-nil."
|
||||
[db-model user-id context card-type keep]
|
||||
[db-model :- :string
|
||||
user-id :- ::lib.schema.id/user
|
||||
context :- :string
|
||||
card-type :- [:maybe :string]
|
||||
keep :- ms/IntGreaterThanOrEqualToZero]
|
||||
(t2/select-fn-set :id
|
||||
:model/RecentViews
|
||||
{:select [:rv.id]
|
||||
@@ -163,19 +172,20 @@
|
||||
:limit 100000
|
||||
:offset keep}))
|
||||
|
||||
(defn insert-recent-views!
|
||||
(mu/defn insert-recent-views!
|
||||
"Insert the RecentViews `rows`."
|
||||
[rows]
|
||||
[rows :- [:sequential ::activity-feed.schema/recent-views.update]]
|
||||
(t2/insert! :model/RecentViews rows))
|
||||
|
||||
(defn delete-recent-views!
|
||||
(mu/defn delete-recent-views!
|
||||
"Delete the RecentViews with `ids`."
|
||||
[ids]
|
||||
[ids :- [:set ms/PositiveInt]]
|
||||
(t2/delete! :model/RecentViews :id [:in ids]))
|
||||
|
||||
(defn most-recently-viewed-dashboard-id
|
||||
(mu/defn most-recently-viewed-dashboard-id
|
||||
"The id of the unarchived Dashboard the User with `user-id` viewed most recently after `since`, or nil."
|
||||
[user-id since]
|
||||
[user-id :- ::lib.schema.id/user
|
||||
since :- ms/TemporalInstant]
|
||||
(t2/select-one-fn
|
||||
:model_id
|
||||
:model/RecentViews
|
||||
@@ -188,9 +198,9 @@
|
||||
:left-join [[:report_dashboard :d]
|
||||
[:= :recent_views.model_id :d.id]]}))
|
||||
|
||||
(defn cards-for-recent-views
|
||||
(mu/defn cards-for-recent-views
|
||||
"The Cards with `card-ids` with their Dashboard, Collection, and moderation status."
|
||||
[card-ids]
|
||||
[card-ids :- [:sequential ::lib.schema.id/card]]
|
||||
(t2/select :model/Card
|
||||
{:select [:card.name
|
||||
:card.description
|
||||
@@ -224,9 +234,9 @@
|
||||
[:report_dashboard :dashboard]
|
||||
[:= :dashboard.id :card.dashboard_id]]}))
|
||||
|
||||
(defn dashboards-for-recent-views
|
||||
(mu/defn dashboards-for-recent-views
|
||||
"The Dashboards with `dashboard-ids` with their Collection and moderation status."
|
||||
[dashboard-ids]
|
||||
[dashboard-ids :- [:sequential ::lib.schema.id/dashboard]]
|
||||
(t2/select :model/Dashboard
|
||||
{:select [:dash.id
|
||||
:dash.name
|
||||
@@ -249,9 +259,9 @@
|
||||
[:= :c.id :dash.collection_id]
|
||||
[:= :c.archived false]]]}))
|
||||
|
||||
(defn unarchived-collections-with-details
|
||||
(mu/defn unarchived-collections-with-details
|
||||
"The unarchived Collections with `collection-ids`, with their location, type, and authority level."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:sequential ::lib.schema.id/collection]]
|
||||
(t2/select :model/Collection
|
||||
{:select [:id :name :description :authority_level
|
||||
:archived :location :type]
|
||||
@@ -259,9 +269,9 @@
|
||||
[:in :id collection-ids]
|
||||
[:= :archived false]]}))
|
||||
|
||||
(defn visible-tables-for-recent-views
|
||||
(mu/defn visible-tables-for-recent-views
|
||||
"The non-hidden Tables with `table-ids` with their Database name and sync status."
|
||||
[table-ids]
|
||||
[table-ids :- [:sequential ::lib.schema.id/table]]
|
||||
(t2/select :model/Table
|
||||
{:select [:t.id :t.name :t.description
|
||||
:t.display_name :t.active :t.visibility_type :t.schema
|
||||
@@ -277,11 +287,15 @@
|
||||
:left-join [[:metabase_database :db]
|
||||
[:= :db.id :t.db_id]]}))
|
||||
|
||||
(defn recent-views-with-card-type
|
||||
(mu/defn recent-views-with-card-type
|
||||
"The RecentViews of the User with `user-id` in `contexts`, newest first, with the type of the viewed Card. Narrowed
|
||||
to `db-models` and to the Cards of `card-types` when given; excludes trashed and namespaced Collections, exploration
|
||||
Documents, and, when `selections?`, the instance analytics Collection."
|
||||
[user-id contexts db-models card-types selections?]
|
||||
[user-id :- ::lib.schema.id/user
|
||||
contexts :- [:sequential :string]
|
||||
db-models :- [:maybe [:sequential :string]]
|
||||
card-types :- [:sequential :string]
|
||||
selections? :- :boolean]
|
||||
(t2/select :model/RecentViews
|
||||
{:select [:rv.* [:rc.type :card_type]]
|
||||
:from [[:recent_views :rv]]
|
||||
@@ -325,9 +339,9 @@
|
||||
[:= :doc.id :rv.model_id]]]
|
||||
:order-by [[:rv.timestamp :desc]]}))
|
||||
|
||||
(defn documents-for-recent-views
|
||||
(mu/defn documents-for-recent-views
|
||||
"The Documents with `document-ids` with their Collection."
|
||||
[document-ids]
|
||||
[document-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select :model/Document
|
||||
{:select [:d.id
|
||||
:d.name
|
||||
|
||||
25
src/metabase/activity_feed/schema.clj
Normal file
25
src/metabase/activity_feed/schema.clj
Normal file
@@ -0,0 +1,25 @@
|
||||
(ns metabase.activity-feed.schema
|
||||
"Malli schemas for the activity-feed module."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
(mr/def ::recent-views
|
||||
"A RecentViews as selected from the app DB: every column of `:recent_views`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:user_id ::lib.schema.id/user]
|
||||
[:model [:or :keyword :string]]
|
||||
[:model_id [:maybe :int]]
|
||||
[:timestamp ms/TemporalInstant]
|
||||
[:context [:or :keyword :string]]])
|
||||
|
||||
(mr/def ::recent-views.update
|
||||
"What an update (or insert) of a RecentViews accepts: every column of `:recent_views` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
|
||||
[:model {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:model_id {:optional true} [:maybe :int]]
|
||||
[:timestamp {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:context {:optional true} [:maybe [:or :keyword :string]]]])
|
||||
@@ -2,71 +2,78 @@
|
||||
"Application database queries for the agent API module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module only touches `toucan2.core` for hydration."
|
||||
(:require
|
||||
[malli.util :as mut]
|
||||
[metabase.dashboards.schema :as dashboards.schema]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util :as u]
|
||||
[metabase.util.malli :as mu]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn collection-breadcrumb-columns
|
||||
(mu/defn collection-breadcrumb-columns
|
||||
"The id, name, location, owner, namespace, and archival of the Collection with `collection-id`, or nil."
|
||||
[collection-id]
|
||||
[collection-id :- ::lib.schema.id/collection]
|
||||
(t2/select-one [:model/Collection :id :name :location :personal_owner_id :namespace :archived_directly]
|
||||
collection-id))
|
||||
|
||||
(defn card
|
||||
(mu/defn card
|
||||
"The Card with `card-id`, or nil."
|
||||
[card-id]
|
||||
[card-id :- ::lib.schema.id/card]
|
||||
(t2/select-one :model/Card :id card-id))
|
||||
|
||||
(defn dashboard
|
||||
(mu/defn dashboard
|
||||
"The Dashboard with `dashboard-id`, or nil."
|
||||
[dashboard-id]
|
||||
[dashboard-id :- ::lib.schema.id/dashboard]
|
||||
(t2/select-one :model/Dashboard :id dashboard-id))
|
||||
|
||||
(defn dashboard-tab-names
|
||||
(mu/defn dashboard-tab-names
|
||||
"The id and name of the DashboardTabs of the Dashboard with `dashboard-id`, in position order."
|
||||
[dashboard-id]
|
||||
[dashboard-id :- ::lib.schema.id/dashboard]
|
||||
(t2/select [:model/DashboardTab :id :name] :dashboard_id dashboard-id {:order-by [[:position :asc] [:id :asc]]}))
|
||||
|
||||
(defn dashboard-tab-ids
|
||||
(mu/defn dashboard-tab-ids
|
||||
"The ids of the DashboardTabs of the Dashboard with `dashboard-id`, in position order."
|
||||
[dashboard-id]
|
||||
[dashboard-id :- ::lib.schema.id/dashboard]
|
||||
(t2/select-pks-vec :model/DashboardTab :dashboard_id dashboard-id {:order-by [[:position :asc] [:id :asc]]}))
|
||||
|
||||
(defn insert-dashboard!
|
||||
(mu/defn insert-dashboard!
|
||||
"Insert the Dashboard `row` and return the inserted instance."
|
||||
[row]
|
||||
[row :- ::dashboards.schema/dashboard.update]
|
||||
(t2/insert-returning-instance! :model/Dashboard row))
|
||||
|
||||
(defn insert-dashcard!
|
||||
(mu/defn insert-dashcard!
|
||||
"Insert the DashboardCard `row` and return the inserted instance."
|
||||
[row]
|
||||
[row :- ::dashboards.schema/dashboard-card.update]
|
||||
(t2/insert-returning-instance! :model/DashboardCard row))
|
||||
|
||||
(defn dashcard-ids-in-layout-order
|
||||
(mu/defn dashcard-ids-in-layout-order
|
||||
"The ids of the DashboardCards of the Dashboard with `dashboard-id`, in row then column order."
|
||||
[dashboard-id]
|
||||
[dashboard-id :- ::lib.schema.id/dashboard]
|
||||
(t2/select-fn-vec :id :model/DashboardCard :dashboard_id dashboard-id {:order-by [[:row :asc] [:col :asc]]}))
|
||||
|
||||
(defn dashcards
|
||||
(mu/defn dashcards
|
||||
"The DashboardCards of the Dashboard with `dashboard-id`."
|
||||
[dashboard-id]
|
||||
[dashboard-id :- ::lib.schema.id/dashboard]
|
||||
(t2/select :model/DashboardCard :dashboard_id dashboard-id))
|
||||
|
||||
(defn dashcard-in-dashboard
|
||||
(mu/defn dashcard-in-dashboard
|
||||
"The DashboardCard with `dashcard-id` on the Dashboard with `dashboard-id`, or nil."
|
||||
[dashcard-id dashboard-id]
|
||||
[dashcard-id :- ::lib.schema.id/dashcard
|
||||
dashboard-id :- ::lib.schema.id/dashboard]
|
||||
(t2/select-one :model/DashboardCard :id dashcard-id :dashboard_id dashboard-id))
|
||||
|
||||
(defn update-dashcard!
|
||||
"Apply `changes` to the DashboardCard with `dashcard-id`."
|
||||
[dashcard-id changes]
|
||||
(mu/defn update-dashcard!
|
||||
"Apply `changes` to the DashboardCard with `dashcard-id`, returning the number updated."
|
||||
[dashcard-id :- ::lib.schema.id/dashcard
|
||||
changes :- (mut/select-keys ::dashboards.schema/dashboard-card.update [:visualization_settings :row :col])]
|
||||
(t2/update! :model/DashboardCard dashcard-id changes))
|
||||
|
||||
(defn update-dashboard!
|
||||
"Apply `changes` to the Dashboard with `dashboard-id`."
|
||||
[dashboard-id changes]
|
||||
(mu/defn update-dashboard!
|
||||
"Apply `changes` to the Dashboard with `dashboard-id`, returning the number updated."
|
||||
[dashboard-id :- ::lib.schema.id/dashboard
|
||||
changes :- (mut/select-keys ::dashboards.schema/dashboard.update [:name :description :collection_id :archived :archived_directly])]
|
||||
(t2/update! :model/Dashboard dashboard-id changes))
|
||||
|
||||
(defn active-user-by-email
|
||||
(mu/defn active-user-by-email
|
||||
"The active User with `email`, compared case-insensitively, or nil."
|
||||
[email]
|
||||
[email :- :string]
|
||||
(t2/select-one :model/User :%lower.email (u/lower-case-en email) :is_active true))
|
||||
|
||||
33
src/metabase/agent_api/schema.clj
Normal file
33
src/metabase/agent_api/schema.clj
Normal file
@@ -0,0 +1,33 @@
|
||||
(ns metabase.agent-api.schema
|
||||
"Malli schemas for the agent-api module."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
(mr/def ::agent-api-call-log
|
||||
"A AgentApiCallLog as selected from the app DB: every column of `:agent_api_call_log`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:created_at ms/TemporalInstant]
|
||||
[:user_id [:maybe ::lib.schema.id/user]]
|
||||
[:tenant_id [:maybe ms/PositiveInt]]
|
||||
[:client_name [:maybe :string]]
|
||||
[:operation [:maybe :string]]
|
||||
[:status [:maybe [:or :keyword :string]]]
|
||||
[:duration_ms [:maybe :int]]
|
||||
[:ip_address [:maybe :string]]
|
||||
[:error_message [:maybe :string]]])
|
||||
|
||||
(mr/def ::agent-api-call-log.update
|
||||
"What an update (or insert) of a AgentApiCallLog accepts: every column of `:agent_api_call_log` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
|
||||
[:tenant_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:client_name {:optional true} [:maybe :string]]
|
||||
[:operation {:optional true} [:maybe :string]]
|
||||
[:status {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:duration_ms {:optional true} [:maybe :int]]
|
||||
[:ip_address {:optional true} [:maybe :string]]
|
||||
[:error_message {:optional true} [:maybe :string]]])
|
||||
@@ -4,72 +4,75 @@
|
||||
(:require
|
||||
[clojure.string :as str]
|
||||
[metabase.app-db.core :as app-db]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.models.interface :as mi]
|
||||
[metabase.util.honey-sql-2 :as h2x]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn first-user-date-joined
|
||||
(mu/defn first-user-date-joined
|
||||
"The earliest join date among all Users, or nil."
|
||||
[]
|
||||
(t2/select-one-fn :min [:model/User [:%min.date_joined :min]]))
|
||||
|
||||
(defn sample-database-exists?
|
||||
(mu/defn sample-database-exists?
|
||||
"Whether a sample Database exists."
|
||||
[]
|
||||
(t2/exists? :model/Database, :is_sample true))
|
||||
|
||||
(defn sample-database-id
|
||||
(mu/defn sample-database-id
|
||||
"The id of the sample Database, or nil."
|
||||
[]
|
||||
(t2/select-one-pk :model/Database :is_sample true))
|
||||
|
||||
(defn personal-user-stats-columns
|
||||
(mu/defn personal-user-stats-columns
|
||||
"The active, superuser, last login, and SSO source of every personal User."
|
||||
[]
|
||||
(t2/select [:model/User :is_active :is_superuser :last_login :sso_source] :type :personal))
|
||||
|
||||
(defn document-archived-flags
|
||||
(mu/defn document-archived-flags
|
||||
"The archived flag of every Document."
|
||||
[]
|
||||
(t2/select [:model/Document :archived]))
|
||||
|
||||
(defn collection-by-type
|
||||
(mu/defn collection-by-type
|
||||
"The id and location of a Collection of `collection-type`, or nil."
|
||||
[collection-type]
|
||||
[collection-type :- :string]
|
||||
(t2/select-one [:model/Collection :id :location] :type collection-type))
|
||||
|
||||
(defn descendant-collection-ids
|
||||
(mu/defn descendant-collection-ids
|
||||
"The ids of the Collections whose location starts with `location-prefix`, or nil."
|
||||
[location-prefix]
|
||||
[location-prefix :- :string]
|
||||
(t2/select-pks-set :model/Collection :location [:like (str location-prefix "%")]))
|
||||
|
||||
(defn published-table-count-in-collections
|
||||
(mu/defn published-table-count-in-collections
|
||||
"The number of published Tables in the Collections with `collection-ids`."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:set ::lib.schema.id/collection]]
|
||||
(t2/count :model/Table {:where [:and
|
||||
[:= :is_published true]
|
||||
[:in :collection_id collection-ids]]}))
|
||||
|
||||
(defn unarchived-metric-count-in-collections
|
||||
(mu/defn unarchived-metric-count-in-collections
|
||||
"The number of unarchived metric Cards in the Collections with `collection-ids`."
|
||||
[collection-ids]
|
||||
[collection-ids :- [:set ::lib.schema.id/collection]]
|
||||
(t2/count :model/Card {:where [:and
|
||||
[:= :type "metric"]
|
||||
[:= :archived false]
|
||||
[:in :collection_id collection-ids]]}))
|
||||
|
||||
(defn permissions-group-count
|
||||
(mu/defn permissions-group-count
|
||||
"The number of PermissionsGroups."
|
||||
[]
|
||||
(t2/count :model/PermissionsGroup))
|
||||
|
||||
(defn dashboard-stats-columns
|
||||
(mu/defn dashboard-stats-columns
|
||||
"The creator, public uuid, parameters, and embedding columns of the non-internal Dashboards."
|
||||
[]
|
||||
(t2/select [:model/Dashboard :creator_id :public_uuid :parameters :enable_embedding :embedding_params]
|
||||
{:where (mi/exclude-internal-content-hsql :model/Dashboard)}))
|
||||
|
||||
(defn dashcards-of-dashboards
|
||||
(mu/defn dashcards-of-dashboards
|
||||
"The DashboardCards of the non-internal Dashboards."
|
||||
[]
|
||||
(t2/query {:select :dc.*
|
||||
@@ -84,33 +87,36 @@
|
||||
:where [(if alerts? :not= :=) :pulse.alert_condition nil]}
|
||||
left-join? (assoc :left-join [:pulse [:= :pulse.id :pulse_id]]))))
|
||||
|
||||
(defn pulse-channel-frequencies-by-column
|
||||
(mu/defn pulse-channel-frequencies-by-column
|
||||
"The distinct `column` values (as `:k`) and their `:count` among the PulseChannels of alerts when `alerts?`, or of
|
||||
pulses otherwise."
|
||||
[column alerts?]
|
||||
[column :- :keyword
|
||||
alerts? :- :boolean]
|
||||
(notification-frequencies-by-column* :model/PulseChannel column alerts? true))
|
||||
|
||||
(defn pulse-frequencies-by-column
|
||||
(mu/defn pulse-frequencies-by-column
|
||||
"The distinct `column` values (as `:k`) and their `:count` among the Pulses of alerts when `alerts?`, or of pulses
|
||||
otherwise."
|
||||
[column alerts?]
|
||||
[column :- :keyword
|
||||
alerts? :- :boolean]
|
||||
(notification-frequencies-by-column* :model/Pulse column alerts? false))
|
||||
|
||||
(defn pulse-card-frequencies-by-column
|
||||
(mu/defn pulse-card-frequencies-by-column
|
||||
"The distinct `column` values (as `:k`) and their `:count` among the PulseCards of alerts when `alerts?`, or of
|
||||
pulses otherwise."
|
||||
[column alerts?]
|
||||
[column :- :keyword
|
||||
alerts? :- :boolean]
|
||||
(notification-frequencies-by-column* :model/PulseCard column alerts? true))
|
||||
|
||||
(defn pulse-count
|
||||
(mu/defn pulse-count
|
||||
"The number of Pulses that are not alerts."
|
||||
[]
|
||||
(t2/count :model/Pulse :alert_condition nil))
|
||||
|
||||
(defn notification-xls-or-csv-card-count
|
||||
(mu/defn notification-xls-or-csv-card-count
|
||||
"The `:count` of Notifications with at least one PulseCard with `:include_xls` or `:include_csv`, of alerts when
|
||||
`alerts?`, or of pulses otherwise."
|
||||
[alerts?]
|
||||
[alerts? :- :boolean]
|
||||
(t2/query-one {:select [[[::h2x/distinct-count :pulse.id] :count]]
|
||||
:from [:pulse]
|
||||
:left-join [:pulse_card [:= :pulse.id :pulse_card.pulse_id]]
|
||||
@@ -120,38 +126,38 @@
|
||||
[:= :pulse_card.include_xls true]]
|
||||
[(if alerts? :not= :=) :alert_condition nil]]}))
|
||||
|
||||
(defn alert-count
|
||||
(mu/defn alert-count
|
||||
"The number of Pulses that are alerts."
|
||||
[]
|
||||
(t2/count :model/Pulse :alert_condition [:not= nil]))
|
||||
|
||||
(defn first-time-only-alert-count
|
||||
(mu/defn first-time-only-alert-count
|
||||
"The number of alert Pulses that fire only once."
|
||||
[]
|
||||
(t2/count :model/Pulse :alert_condition [:not= nil], :alert_first_only true))
|
||||
|
||||
(defn above-goal-alert-count
|
||||
(mu/defn above-goal-alert-count
|
||||
"The number of alert Pulses that fire above their goal."
|
||||
[]
|
||||
(t2/count :model/Pulse :alert_condition [:not= nil], :alert_above_goal true))
|
||||
|
||||
(defn collection-count
|
||||
(mu/defn collection-count
|
||||
"The number of non-internal Collections."
|
||||
[]
|
||||
(t2/count :model/Collection {:where (mi/exclude-internal-content-hsql :model/Collection)}))
|
||||
|
||||
(defn card-collection-ids
|
||||
(mu/defn card-collection-ids
|
||||
"The Collection id and schema of the non-internal Cards."
|
||||
[]
|
||||
(t2/select [:model/Card :collection_id :card_schema] {:where [:and (mi/exclude-internal-content-hsql :model/Card)]}))
|
||||
|
||||
(defn database-stats-columns
|
||||
(mu/defn database-stats-columns
|
||||
"The sync, engine, and DBMS version of the non-internal Databases."
|
||||
[]
|
||||
(t2/select [:model/Database :is_full_sync :engine :dbms_version]
|
||||
{:where (mi/exclude-internal-content-hsql :model/Database)}))
|
||||
|
||||
(defn table-database-and-schema
|
||||
(mu/defn table-database-and-schema
|
||||
"The Database id and schema of the Tables of the non-internal Databases."
|
||||
[]
|
||||
(t2/query {:select [:t.db_id :t.schema]
|
||||
@@ -159,7 +165,7 @@
|
||||
:join [[(t2/table-name :model/Database) :d] [:= :d.id :t.db_id]]
|
||||
:where (mi/exclude-internal-content-hsql :model/Database :table-alias :d)}))
|
||||
|
||||
(defn field-table-ids
|
||||
(mu/defn field-table-ids
|
||||
"The Table id of the Fields of the non-internal Databases."
|
||||
[]
|
||||
(t2/query {:select [:f.table_id]
|
||||
@@ -168,12 +174,12 @@
|
||||
[(t2/table-name :model/Database) :d] [:= :d.id :t.db_id]]
|
||||
:where (mi/exclude-internal-content-hsql :model/Database :table-alias :d)}))
|
||||
|
||||
(defn segment-count
|
||||
(mu/defn segment-count
|
||||
"The number of Segments."
|
||||
[]
|
||||
(t2/count :model/Segment))
|
||||
|
||||
(defn unarchived-metric-card-count
|
||||
(mu/defn unarchived-metric-card-count
|
||||
"The number of unarchived metric Cards."
|
||||
[]
|
||||
(t2/count :model/Card :type :metric :archived false))
|
||||
@@ -230,149 +236,151 @@
|
||||
")"
|
||||
"SELECT q1.*, q2.* FROM query_stats_1 q1, query_stats_2 q2;"])))
|
||||
|
||||
(defn execution-metrics
|
||||
(mu/defn execution-metrics
|
||||
"The execution statistics over the last 30 days of QueryExecutions."
|
||||
[]
|
||||
(first (t2/query (execution-metrics-sql))))
|
||||
|
||||
(defn query-cache-stats
|
||||
(mu/defn query-cache-stats
|
||||
"The average result `:length` and `:count` of the QueryCache entries."
|
||||
[]
|
||||
(t2/select-one [:model/QueryCache [[:avg [:length :results]] :length] [:%count.* :count]]))
|
||||
|
||||
(defn user-count-joined-before
|
||||
(mu/defn user-count-joined-before
|
||||
"The number (up to `limit`) of non-internal Users who joined at or before `joined-before`."
|
||||
[joined-before limit]
|
||||
[joined-before :- ms/TemporalInstant
|
||||
limit :- ms/PositiveInt]
|
||||
(t2/count :model/User {:where [:and
|
||||
[:<= :date_joined joined-before]
|
||||
(mi/exclude-internal-content-hsql :model/User)]
|
||||
:limit limit}))
|
||||
|
||||
(defn query-execution-ids-excluding-database
|
||||
(mu/defn query-execution-ids-excluding-database
|
||||
"Up to `limit` ids of QueryExecutions not run against the Database with `database-id`."
|
||||
[database-id limit]
|
||||
[database-id :- [:maybe ::lib.schema.id/database]
|
||||
limit :- ms/PositiveInt]
|
||||
(t2/select-fn-set :id :model/QueryExecution
|
||||
{:where [:or
|
||||
[:not= :database_id database-id]
|
||||
[:= :database_id nil]]
|
||||
:limit limit}))
|
||||
|
||||
(defn transform-count
|
||||
(mu/defn transform-count
|
||||
"The number of Transforms."
|
||||
[]
|
||||
(t2/count :model/Transform))
|
||||
|
||||
(defn transform-run-count-since
|
||||
(mu/defn transform-run-count-since
|
||||
"The number of TransformRuns started at or after `since`."
|
||||
[since]
|
||||
[since :- ms/TemporalInstant]
|
||||
(t2/count :model/TransformRun :start_time [:>= since]))
|
||||
|
||||
(defn unarchived-model-count
|
||||
(mu/defn unarchived-model-count
|
||||
"The number of unarchived model Cards."
|
||||
[]
|
||||
(t2/count :model/Card :type :model :archived false))
|
||||
|
||||
(defn new-embedded-dashboard-count-since
|
||||
(mu/defn new-embedded-dashboard-count-since
|
||||
"The number of unarchived Dashboards with embedding enabled created at or after `since`."
|
||||
[since]
|
||||
[since :- ms/TemporalInstant]
|
||||
(t2/count :model/Dashboard :enable_embedding true :archived false :created_at [:>= since]))
|
||||
|
||||
(defn new-active-user-count-since
|
||||
(mu/defn new-active-user-count-since
|
||||
"The number of active Users who joined at or after `since`."
|
||||
[since]
|
||||
[since :- ms/TemporalInstant]
|
||||
(t2/count :model/User :is_active true :date_joined [:>= since]))
|
||||
|
||||
(defn unarchived-pivot-table-count
|
||||
(mu/defn unarchived-pivot-table-count
|
||||
"The number of unarchived pivot table Cards."
|
||||
[]
|
||||
(t2/count :model/Card :display :pivot :archived false))
|
||||
|
||||
(defn query-execution-count-since
|
||||
(mu/defn query-execution-count-since
|
||||
"The number of QueryExecutions started at or after `since`."
|
||||
[since]
|
||||
[since :- ms/TemporalInstant]
|
||||
(t2/count :model/QueryExecution :started_at [:>= since]))
|
||||
|
||||
(defn new-scim-user-count-since
|
||||
(mu/defn new-scim-user-count-since
|
||||
"The number of active SCIM-provisioned Users who joined at or after `since`."
|
||||
[since]
|
||||
[since :- ms/TemporalInstant]
|
||||
(t2/count :model/User :sso_source :scim :is_active true :date_joined [:>= since]))
|
||||
|
||||
(defn database-engines-among
|
||||
(mu/defn database-engines-among
|
||||
"The set of engines of the Databases whose engine is one of `engine-names`."
|
||||
[engine-names]
|
||||
[engine-names :- [:sequential :string]]
|
||||
(t2/select-fn-set :engine :model/Database {:where [:in :engine engine-names]}))
|
||||
|
||||
(defn embedded-dashboard-exists?
|
||||
(mu/defn embedded-dashboard-exists?
|
||||
"Whether a Dashboard with embedding enabled exists."
|
||||
[]
|
||||
(t2/exists? :model/Dashboard :enable_embedding true))
|
||||
|
||||
(defn embedded-card-exists?
|
||||
(mu/defn embedded-card-exists?
|
||||
"Whether a Card with embedding enabled exists."
|
||||
[]
|
||||
(t2/exists? :model/Card :enable_embedding true))
|
||||
|
||||
(defn public-dashboard-exists?
|
||||
(mu/defn public-dashboard-exists?
|
||||
"Whether a publicly shared Dashboard exists."
|
||||
[]
|
||||
(t2/exists? :model/Dashboard :public_uuid [:not= nil]))
|
||||
|
||||
(defn public-card-exists?
|
||||
(mu/defn public-card-exists?
|
||||
"Whether a publicly shared Card exists."
|
||||
[]
|
||||
(t2/exists? :model/Card :public_uuid [:not= nil]))
|
||||
|
||||
(defn custom-viz-plugin-exists?
|
||||
(mu/defn custom-viz-plugin-exists?
|
||||
"Whether a CustomVizPlugin exists."
|
||||
[]
|
||||
(t2/exists? :model/CustomVizPlugin))
|
||||
|
||||
(defn uploads-database-exists?
|
||||
(mu/defn uploads-database-exists?
|
||||
"Whether a Database with uploads enabled exists."
|
||||
[]
|
||||
(t2/exists? :model/Database :uploads_enabled true))
|
||||
|
||||
(defn official-collection-exists?
|
||||
(mu/defn official-collection-exists?
|
||||
"Whether an official Collection exists."
|
||||
[]
|
||||
(t2/exists? :model/Collection :authority_level "official"))
|
||||
|
||||
(defn cache-config-exists?
|
||||
(mu/defn cache-config-exists?
|
||||
"Whether any CacheConfig exists."
|
||||
[]
|
||||
(t2/exists? :model/CacheConfig))
|
||||
|
||||
(defn preemptive-cache-config-exists?
|
||||
(mu/defn preemptive-cache-config-exists?
|
||||
"Whether a CacheConfig that refreshes automatically exists."
|
||||
[]
|
||||
(t2/exists? :model/CacheConfig :refresh_automatically true))
|
||||
|
||||
(defn database-router-exists?
|
||||
(mu/defn database-router-exists?
|
||||
"Whether a DatabaseRouter exists."
|
||||
[]
|
||||
(t2/exists? :model/DatabaseRouter))
|
||||
|
||||
(defn moderation-review-exists?
|
||||
(mu/defn moderation-review-exists?
|
||||
"Whether a ModerationReview exists."
|
||||
[]
|
||||
(t2/exists? :model/ModerationReview))
|
||||
|
||||
(defn filtered-pulse-exists?
|
||||
(mu/defn filtered-pulse-exists?
|
||||
"Whether a Pulse with parameters exists."
|
||||
[]
|
||||
(t2/exists? :model/Pulse {:where [:not= :parameters "[]"]}))
|
||||
|
||||
(defn upload-table-exists?
|
||||
(mu/defn upload-table-exists?
|
||||
"Whether an uploaded Table exists."
|
||||
[]
|
||||
(t2/exists? :model/Table :is_upload true))
|
||||
|
||||
(defn snippet-collection-exists?
|
||||
(mu/defn snippet-collection-exists?
|
||||
"Whether a snippet Collection exists."
|
||||
[]
|
||||
(t2/exists? :model/Collection :namespace "snippets"))
|
||||
|
||||
(defn starburst-database-details
|
||||
(mu/defn starburst-database-details
|
||||
"The connection details of the Starburst Databases."
|
||||
[]
|
||||
(t2/select-fn-set :details :model/Database :engine "starburst"))
|
||||
|
||||
@@ -2,16 +2,19 @@
|
||||
"Application database queries for the API module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn entity-exists?
|
||||
(mu/defn entity-exists?
|
||||
"Whether a row of `entity` matching the key-value `conditions` exists."
|
||||
[entity & conditions]
|
||||
[entity :- :keyword & conditions :- [:* :some]]
|
||||
(apply t2/exists? entity conditions))
|
||||
|
||||
(defn entity-by-id
|
||||
(mu/defn entity-by-id
|
||||
"The `entity` row with `id` also matching the key-value `conditions`, or nil."
|
||||
[entity id & conditions]
|
||||
[entity :- :keyword id :- [:or ms/PositiveInt :string] & conditions :- [:* :some]]
|
||||
(apply t2/select-one entity :id id conditions))
|
||||
|
||||
(defn- shift-positions-after!
|
||||
@@ -32,74 +35,102 @@
|
||||
:collection_position [:between lower upper]}
|
||||
{:collection_position [plus-or-minus :collection_position 1]}))
|
||||
|
||||
(defn shift-card-positions-after!
|
||||
(mu/defn shift-card-positions-after!
|
||||
"Add or subtract (`plus-or-minus`) one from the collection position of the Cards in the Collection with
|
||||
`collection-id` positioned after `position`."
|
||||
[collection-id position plus-or-minus]
|
||||
[collection-id :- [:maybe ::lib.schema.id/collection]
|
||||
position :- ms/PositiveInt
|
||||
plus-or-minus :- [:enum :+ :-]]
|
||||
(shift-positions-after! 'Card collection-id position plus-or-minus))
|
||||
|
||||
(defn shift-dashboard-positions-after!
|
||||
(mu/defn shift-dashboard-positions-after!
|
||||
"Add or subtract (`plus-or-minus`) one from the collection position of the Dashboards in the Collection with
|
||||
`collection-id` positioned after `position`."
|
||||
[collection-id position plus-or-minus]
|
||||
[collection-id :- [:maybe ::lib.schema.id/collection]
|
||||
position :- ms/PositiveInt
|
||||
plus-or-minus :- [:enum :+ :-]]
|
||||
(shift-positions-after! 'Dashboard collection-id position plus-or-minus))
|
||||
|
||||
(defn shift-pulse-positions-after!
|
||||
(mu/defn shift-pulse-positions-after!
|
||||
"Add or subtract (`plus-or-minus`) one from the collection position of the Pulses in the Collection with
|
||||
`collection-id` positioned after `position`."
|
||||
[collection-id position plus-or-minus]
|
||||
[collection-id :- [:maybe ::lib.schema.id/collection]
|
||||
position :- ms/PositiveInt
|
||||
plus-or-minus :- [:enum :+ :-]]
|
||||
(shift-positions-after! 'Pulse collection-id position plus-or-minus))
|
||||
|
||||
(defn shift-document-positions-after!
|
||||
(mu/defn shift-document-positions-after!
|
||||
"Add or subtract (`plus-or-minus`) one from the collection position of the Documents in the Collection with
|
||||
`collection-id` positioned after `position`."
|
||||
[collection-id position plus-or-minus]
|
||||
[collection-id :- [:maybe ::lib.schema.id/collection]
|
||||
position :- ms/PositiveInt
|
||||
plus-or-minus :- [:enum :+ :-]]
|
||||
(shift-positions-after! 'Document collection-id position plus-or-minus))
|
||||
|
||||
(defn shift-card-positions-from!
|
||||
(mu/defn shift-card-positions-from!
|
||||
"Add or subtract (`plus-or-minus`) one from the collection position of the Cards in the Collection with
|
||||
`collection-id` positioned at or after `position`."
|
||||
[collection-id position plus-or-minus]
|
||||
[collection-id :- [:maybe ::lib.schema.id/collection]
|
||||
position :- ms/PositiveInt
|
||||
plus-or-minus :- [:enum :+ :-]]
|
||||
(shift-positions-from! 'Card collection-id position plus-or-minus))
|
||||
|
||||
(defn shift-dashboard-positions-from!
|
||||
(mu/defn shift-dashboard-positions-from!
|
||||
"Add or subtract (`plus-or-minus`) one from the collection position of the Dashboards in the Collection with
|
||||
`collection-id` positioned at or after `position`."
|
||||
[collection-id position plus-or-minus]
|
||||
[collection-id :- [:maybe ::lib.schema.id/collection]
|
||||
position :- ms/PositiveInt
|
||||
plus-or-minus :- [:enum :+ :-]]
|
||||
(shift-positions-from! 'Dashboard collection-id position plus-or-minus))
|
||||
|
||||
(defn shift-pulse-positions-from!
|
||||
(mu/defn shift-pulse-positions-from!
|
||||
"Add or subtract (`plus-or-minus`) one from the collection position of the Pulses in the Collection with
|
||||
`collection-id` positioned at or after `position`."
|
||||
[collection-id position plus-or-minus]
|
||||
[collection-id :- [:maybe ::lib.schema.id/collection]
|
||||
position :- ms/PositiveInt
|
||||
plus-or-minus :- [:enum :+ :-]]
|
||||
(shift-positions-from! 'Pulse collection-id position plus-or-minus))
|
||||
|
||||
(defn shift-document-positions-from!
|
||||
(mu/defn shift-document-positions-from!
|
||||
"Add or subtract (`plus-or-minus`) one from the collection position of the Documents in the Collection with
|
||||
`collection-id` positioned at or after `position`."
|
||||
[collection-id position plus-or-minus]
|
||||
[collection-id :- [:maybe ::lib.schema.id/collection]
|
||||
position :- ms/PositiveInt
|
||||
plus-or-minus :- [:enum :+ :-]]
|
||||
(shift-positions-from! 'Document collection-id position plus-or-minus))
|
||||
|
||||
(defn shift-card-positions-between!
|
||||
(mu/defn shift-card-positions-between!
|
||||
"Add or subtract (`plus-or-minus`) one from the collection position of the Cards in the Collection with
|
||||
`collection-id` positioned between `lower` and `upper` inclusive."
|
||||
[collection-id lower upper plus-or-minus]
|
||||
[collection-id :- [:maybe ::lib.schema.id/collection]
|
||||
lower :- ms/PositiveInt
|
||||
upper :- ms/PositiveInt
|
||||
plus-or-minus :- [:enum :+ :-]]
|
||||
(shift-positions-between! 'Card collection-id lower upper plus-or-minus))
|
||||
|
||||
(defn shift-dashboard-positions-between!
|
||||
(mu/defn shift-dashboard-positions-between!
|
||||
"Add or subtract (`plus-or-minus`) one from the collection position of the Dashboards in the Collection with
|
||||
`collection-id` positioned between `lower` and `upper` inclusive."
|
||||
[collection-id lower upper plus-or-minus]
|
||||
[collection-id :- [:maybe ::lib.schema.id/collection]
|
||||
lower :- ms/PositiveInt
|
||||
upper :- ms/PositiveInt
|
||||
plus-or-minus :- [:enum :+ :-]]
|
||||
(shift-positions-between! 'Dashboard collection-id lower upper plus-or-minus))
|
||||
|
||||
(defn shift-pulse-positions-between!
|
||||
(mu/defn shift-pulse-positions-between!
|
||||
"Add or subtract (`plus-or-minus`) one from the collection position of the Pulses in the Collection with
|
||||
`collection-id` positioned between `lower` and `upper` inclusive."
|
||||
[collection-id lower upper plus-or-minus]
|
||||
[collection-id :- [:maybe ::lib.schema.id/collection]
|
||||
lower :- ms/PositiveInt
|
||||
upper :- ms/PositiveInt
|
||||
plus-or-minus :- [:enum :+ :-]]
|
||||
(shift-positions-between! 'Pulse collection-id lower upper plus-or-minus))
|
||||
|
||||
(defn shift-document-positions-between!
|
||||
(mu/defn shift-document-positions-between!
|
||||
"Add or subtract (`plus-or-minus`) one from the collection position of the Documents in the Collection with
|
||||
`collection-id` positioned between `lower` and `upper` inclusive."
|
||||
[collection-id lower upper plus-or-minus]
|
||||
[collection-id :- [:maybe ::lib.schema.id/collection]
|
||||
lower :- ms/PositiveInt
|
||||
upper :- ms/PositiveInt
|
||||
plus-or-minus :- [:enum :+ :-]]
|
||||
(shift-positions-between! 'Document collection-id lower upper plus-or-minus))
|
||||
|
||||
@@ -2,41 +2,53 @@
|
||||
"Application database queries for the API keys module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so no other namespace in the module runs a query itself (model definitions still use `toucan2.core`)."
|
||||
(:require
|
||||
[malli.util :as mut]
|
||||
[metabase.api-keys.schema :as api-keys.schema]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.users.schema :as users.schema]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn unscoped-api-key-count
|
||||
(mu/defn unscoped-api-key-count
|
||||
"The number of ApiKeys without a scope."
|
||||
[]
|
||||
(t2/count :model/ApiKey :scope nil))
|
||||
|
||||
(defn unscoped-api-keys
|
||||
(mu/defn unscoped-api-keys
|
||||
"The ApiKeys without a scope."
|
||||
[]
|
||||
(t2/select :model/ApiKey :scope nil))
|
||||
|
||||
(defn api-key
|
||||
(mu/defn api-key
|
||||
"The ApiKey with `id`, or nil."
|
||||
[id]
|
||||
[id :- ms/PositiveInt]
|
||||
(t2/select-one :model/ApiKey id))
|
||||
|
||||
(defn save-api-key!
|
||||
"Save the changes made to the ApiKey instance `api-key`."
|
||||
[api-key]
|
||||
(def ^:private ApiKeyWithGroupId
|
||||
"An ApiKey instance possibly carrying the `::api-keys/group-id` the before-update hook consumes."
|
||||
(mut/merge ::api-keys.schema/api-key
|
||||
[:map
|
||||
[:metabase.api-keys.core/group-id {:optional true} [:maybe ms/PositiveInt]]]))
|
||||
|
||||
(mu/defn save-api-key!
|
||||
"Save the changes made to the ApiKey instance `api-key` and return it."
|
||||
[api-key :- ApiKeyWithGroupId]
|
||||
(t2/save! api-key))
|
||||
|
||||
(defn api-key-exists?
|
||||
(mu/defn api-key-exists?
|
||||
"Whether an ApiKey with `id` exists."
|
||||
[id]
|
||||
[id :- ms/PositiveInt]
|
||||
(t2/exists? :model/ApiKey id))
|
||||
|
||||
(defn delete-api-key!
|
||||
(mu/defn delete-api-key!
|
||||
"Delete the ApiKey with `id`."
|
||||
[id]
|
||||
[id :- ms/PositiveInt]
|
||||
(t2/delete! :model/ApiKey id))
|
||||
|
||||
(defn api-key-groups
|
||||
(mu/defn api-key-groups
|
||||
"The group name, group id, and api key id of the PermissionsGroups of the ApiKeys with `api-key-ids`."
|
||||
[api-key-ids]
|
||||
[api-key-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/query {:select [[:pg.name :group-name]
|
||||
[:pg.id :group-id]
|
||||
[:api_key.id :api-key-id]]
|
||||
@@ -45,42 +57,44 @@
|
||||
:api_key [:= :api_key.user_id :pgm.user_id]]
|
||||
:where [:in :api_key.id api-key-ids]}))
|
||||
|
||||
(defn rename-api-key-user!
|
||||
(mu/defn rename-api-key-user!
|
||||
"Set the first name (and clear the last name) of the api-key User with `user-id`."
|
||||
[user-id first-name]
|
||||
[user-id :- ::lib.schema.id/user
|
||||
first-name :- :string]
|
||||
(t2/update! :model/User :id user-id, :type :api-key, {:first_name first-name, :last_name ""}))
|
||||
|
||||
(defn user-type
|
||||
(mu/defn user-type
|
||||
"The `:type` of the User with `user-id`, or nil."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one-fn :type :model/User :id user-id))
|
||||
|
||||
(defn deactivate-api-key-user!
|
||||
"Deactivate the api-key User with `user-id`."
|
||||
[user-id]
|
||||
(mu/defn deactivate-api-key-user!
|
||||
"Deactivate the api-key User with `user-id` (nil for keys without a user, e.g. SCIM keys, which updates nothing)."
|
||||
[user-id :- [:maybe ::lib.schema.id/user]]
|
||||
(t2/update! :model/User user-id, :type :api-key, {:is_active false}))
|
||||
|
||||
(defn api-key-prefix-exists?
|
||||
(mu/defn api-key-prefix-exists?
|
||||
"Whether an ApiKey with `prefix` exists."
|
||||
[prefix]
|
||||
[prefix :- :string]
|
||||
(t2/exists? :model/ApiKey :key_prefix prefix))
|
||||
|
||||
(defn api-key-name-exists?
|
||||
(mu/defn api-key-name-exists?
|
||||
"Whether an ApiKey named `key-name` exists."
|
||||
[key-name]
|
||||
[key-name :- :string]
|
||||
(t2/exists? :model/ApiKey :name key-name))
|
||||
|
||||
(defn insert-user!
|
||||
(mu/defn insert-user!
|
||||
"Insert the User `row` and return its id."
|
||||
[row]
|
||||
[row :- ::users.schema/user.update]
|
||||
(t2/insert-returning-pk! :model/User row))
|
||||
|
||||
(defn insert-api-key!
|
||||
(mu/defn insert-api-key!
|
||||
"Insert the ApiKey `row` and return the inserted instance."
|
||||
[row]
|
||||
[row :- ::api-keys.schema/api-key.create]
|
||||
(t2/insert-returning-instance! :model/ApiKey row))
|
||||
|
||||
(defn update-api-key!
|
||||
(mu/defn update-api-key!
|
||||
"Apply `changes` to the ApiKey with `id`."
|
||||
[id changes]
|
||||
[id :- ms/PositiveInt
|
||||
changes :- (mut/select-keys ::api-keys.schema/api-key.update [:key :key_prefix :updated_by_id])]
|
||||
(t2/update! :model/ApiKey :id id changes))
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
[clojure.math :as math]
|
||||
[clojure.string :as str]
|
||||
[malli.core :as mc]
|
||||
[malli.util :as mut]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.users.schema :as users.schema]
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[metabase.util.secret]))
|
||||
@@ -102,7 +105,9 @@
|
||||
[:name [:ref ::name]]
|
||||
[:updated_by_id pos-int?]
|
||||
[:scope {:optional true} [:maybe [:ref ::scope]]]
|
||||
[:single_collection_id {:optional true} [:maybe pos-int?]]])
|
||||
[:single_collection_id {:optional true} [:maybe pos-int?]]
|
||||
;; added by the model's after-select hook
|
||||
[:masked_key {:optional true} :string]])
|
||||
|
||||
(defn- insert-schema [map-schema]
|
||||
(into [:map]
|
||||
@@ -128,3 +133,24 @@
|
||||
|
||||
(mr/def ::api-key.update
|
||||
(update-schema ::api-key))
|
||||
|
||||
(mr/def ::api-key.create
|
||||
"What an insert of a ApiKey accepts: every column of `:api_key` except `id`, all optional, plus `:metabase.api-keys.core/unhashed-key` consumed by the model's hooks."
|
||||
[:map {:closed true}
|
||||
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
|
||||
[:key {:optional true} [:maybe :string]]
|
||||
[:key_prefix {:optional true} [:maybe :string]]
|
||||
[:creator_id {:optional true} [:maybe ::lib.schema.id/user]]
|
||||
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:name {:optional true} [:maybe :string]]
|
||||
[:updated_by_id {:optional true} [:maybe ms/PositiveInt]]
|
||||
[:scope {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:metabase.api-keys.core/unhashed-key {:optional true} ::key.unhashed-or-secret]])
|
||||
|
||||
(mr/def ::api-key.inserted
|
||||
"What inserting an ApiKey returns: the selected row plus the `:group` and `:updated_by` hydrated by the after-insert hook."
|
||||
(mut/merge ::api-key
|
||||
[:map
|
||||
[:group {:optional true} [:map {:closed true} [:id [:maybe pos-int?]] [:name [:maybe :string]]]]
|
||||
[:updated_by {:optional true} ::users.schema/user]]))
|
||||
|
||||
@@ -8,29 +8,34 @@
|
||||
(:require
|
||||
[honey.sql :as sql]
|
||||
[metabase.util.honey-sql-2 :as h2x]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn current-timestamp-string
|
||||
(mu/defn current-timestamp-string
|
||||
"The application DB's own current timestamp, as a string, for app DB type `db-type`."
|
||||
^String [db-type]
|
||||
^String [db-type :- [:enum :h2 :mysql :postgres]]
|
||||
;; for MySQL, cast(current_timestamp AS char); for H2 & Postgres, cast(current_timestamp AS text)
|
||||
(let [cast-form (h2x/cast (if (= db-type :mysql) :char :text) (h2x/current-datetime-honeysql-form db-type))]
|
||||
(:timestamp (t2/query-one {:select [[cast-form :timestamp]]}))))
|
||||
|
||||
;;; ------------------------------------------------ Liquibase ------------------------------------------------
|
||||
|
||||
(defn changelog-by-id
|
||||
(mu/defn changelog-by-id
|
||||
"The Liquibase changelog row with `changelog-id` in the app DB of type `db-type`, or nil."
|
||||
[db-type changelog-id]
|
||||
[db-type :- [:enum :h2 :mysql :postgres]
|
||||
changelog-id :- :string]
|
||||
(let [table-name (case db-type
|
||||
(:postgres :h2) "databasechangelog"
|
||||
:mysql "DATABASECHANGELOG")]
|
||||
(t2/query-one [(format "select * from %s where id = ?" table-name) changelog-id])))
|
||||
|
||||
(defn changelog-ids
|
||||
(mu/defn changelog-ids
|
||||
"The ids among `changelog-ids` still present in the Liquibase changelog table `changelog-table-name`, read on
|
||||
`conn`."
|
||||
[conn changelog-table-name changelog-ids]
|
||||
[conn :- (ms/InstanceOfClass java.sql.Connection)
|
||||
changelog-table-name :- :string
|
||||
changelog-ids :- [:set :string]]
|
||||
(map :id (t2/query conn (sql/format {:select [:id]
|
||||
:from [(keyword changelog-table-name)]
|
||||
:where [:in :id changelog-ids]}))))
|
||||
@@ -40,61 +45,75 @@
|
||||
(def ^:private unmigrated-settings-where
|
||||
[:and [:= :value_with_aad nil] [:not= :value nil] [:not= :value ""]])
|
||||
|
||||
(defn setting-value
|
||||
(mu/defn setting-value
|
||||
"The legacy `value` of the setting row with `setting-key`, or nil."
|
||||
[setting-key]
|
||||
[setting-key :- :string]
|
||||
(t2/select-one-fn :value :setting :key setting-key))
|
||||
|
||||
(defn settings
|
||||
(mu/defn settings
|
||||
"Every setting row, raw."
|
||||
[]
|
||||
(t2/select :setting))
|
||||
|
||||
(defn reducible-setting-values-with-aad
|
||||
(mu/defn reducible-setting-values-with-aad
|
||||
"A reducible of the key and `value_with_aad` of every setting row that has one."
|
||||
[]
|
||||
(t2/reducible-select [:setting :key :value_with_aad] {:where [:!= :value_with_aad nil]}))
|
||||
|
||||
(defn unmigrated-settings?
|
||||
(mu/defn unmigrated-settings?
|
||||
"Whether any setting row has a non-blank `value` but no `value_with_aad`."
|
||||
[]
|
||||
(t2/exists? :setting {:where unmigrated-settings-where}))
|
||||
|
||||
(defn unmigrated-settings
|
||||
(mu/defn unmigrated-settings
|
||||
"Every setting row with a non-blank `value` but no `value_with_aad`, locked for update."
|
||||
[]
|
||||
(t2/select :setting {:where unmigrated-settings-where, :for :update}))
|
||||
|
||||
(defn insert-setting!
|
||||
"Insert a setting row for `setting-key` holding `value` and `value-with-aad`."
|
||||
[setting-key value value-with-aad]
|
||||
(mu/defn insert-setting!
|
||||
"Insert a setting row for `setting-key` holding `value` and `value-with-aad`, returning the number inserted (as a
|
||||
one-element sequence -- `t2/query` on an INSERT, unlike `t2/insert!`, does not unwrap it)."
|
||||
[setting-key :- :string
|
||||
value :- [:maybe :string]
|
||||
value-with-aad :- [:maybe :string]]
|
||||
(t2/query {:insert-into :setting
|
||||
:values [{:key setting-key, :value value, :value_with_aad value-with-aad}]}))
|
||||
|
||||
(defn update-setting-values!
|
||||
(mu/defn update-setting-values!
|
||||
"Set the columns in `changes` -- `:value` and/or `:value_with_aad`, nil writing NULL -- of the setting row with
|
||||
`setting-key`."
|
||||
[setting-key changes]
|
||||
`setting-key`, returning the number updated (as a one-element sequence -- `t2/query` on an UPDATE, unlike
|
||||
`t2/update!`, does not unwrap it)."
|
||||
[setting-key :- :string
|
||||
changes :- [:map {:closed true}
|
||||
[:value {:optional true} [:maybe :string]]
|
||||
[:value_with_aad {:optional true} [:maybe :string]]]]
|
||||
(t2/query {:update :setting
|
||||
:set (select-keys changes [:value :value_with_aad])
|
||||
:where [:= :key setting-key]}))
|
||||
|
||||
(defn delete-setting!
|
||||
"Delete the setting row with `setting-key`."
|
||||
[setting-key]
|
||||
(mu/defn delete-setting!
|
||||
"Delete the setting row with `setting-key`, returning the number deleted (as a one-element sequence -- `t2/query`
|
||||
on a DELETE, unlike `t2/delete!`, does not unwrap it)."
|
||||
[setting-key :- :string]
|
||||
(t2/query {:delete-from :setting, :where [:= :key setting-key]}))
|
||||
|
||||
(defn reducible-column-values
|
||||
(mu/defn reducible-column-values
|
||||
"A reducible of the id and `column` value of every row of `table`."
|
||||
[table column]
|
||||
[table :- :keyword
|
||||
column :- :keyword]
|
||||
(t2/reducible-select [table :id [column :value]]))
|
||||
|
||||
(defn update-column-value!
|
||||
"Set `column` of the row of `table` with `id` to `value`."
|
||||
[table column id value]
|
||||
(mu/defn update-column-value!
|
||||
"Set `column` of the row of `table` with `id` to `value`, returning the number updated (as a one-element
|
||||
sequence -- `t2/query` on an UPDATE, unlike `t2/update!`, does not unwrap it)."
|
||||
[table :- :keyword
|
||||
column :- :keyword
|
||||
id :- ms/PositiveInt
|
||||
value :- [:maybe [:or :string bytes?]]]
|
||||
(t2/query {:update table, :set {column value}, :where [:= :id id]}))
|
||||
|
||||
(defn delete-query-cache!
|
||||
"Delete every cached query result."
|
||||
(mu/defn delete-query-cache!
|
||||
"Delete every cached query result, returning the number deleted (as a one-element sequence -- `t2/query` on a
|
||||
DELETE, unlike `t2/delete!`, does not unwrap it)."
|
||||
[]
|
||||
(t2/query {:delete-from :query_cache}))
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
"Application database queries for the appearance module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so the rest of the module never talks to `toucan2.core` itself."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn unarchived-dashboard-exists?
|
||||
(mu/defn unarchived-dashboard-exists?
|
||||
"Whether an unarchived Dashboard with `dashboard-id` exists."
|
||||
[dashboard-id]
|
||||
[dashboard-id :- ::lib.schema.id/dashboard]
|
||||
(t2/exists? :model/Dashboard :id dashboard-id :archived false))
|
||||
|
||||
@@ -2,41 +2,49 @@
|
||||
"Application database queries for the audit app module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so no other namespace in the module runs a query itself (model definitions still use `toucan2.core`)."
|
||||
(:require
|
||||
[metabase.audit-app.schema :as audit-app.schema]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn cards
|
||||
"The Cards with `card-ids`."
|
||||
[card-ids]
|
||||
(mu/defn cards
|
||||
"The Cards with `card-ids` (nil entries, e.g. from virtual dashcards, are ignored)."
|
||||
[card-ids :- [:sequential [:maybe ::lib.schema.id/card]]]
|
||||
(t2/select :model/Card :id [:in card-ids]))
|
||||
|
||||
(defn audit-log-topic-exists?
|
||||
(mu/defn audit-log-topic-exists?
|
||||
"Whether an AuditLog entry with `topic` exists."
|
||||
[topic]
|
||||
[topic :- [:or :keyword :string]]
|
||||
(t2/exists? :model/AuditLog :topic topic))
|
||||
|
||||
(defn collection-with-entity-id
|
||||
(mu/defn collection-with-entity-id
|
||||
"The Collection with `entity-id`, or nil."
|
||||
[entity-id]
|
||||
[entity-id :- :string]
|
||||
(t2/select-one :model/Collection :entity_id entity-id))
|
||||
|
||||
(defn dashboard-with-entity-id
|
||||
(mu/defn dashboard-with-entity-id
|
||||
"The Dashboard with `entity-id`, or nil."
|
||||
[entity-id]
|
||||
[entity-id :- :string]
|
||||
(t2/select-one :model/Dashboard :entity_id entity-id))
|
||||
|
||||
(defn card-name-and-description
|
||||
(mu/defn card-name-and-description
|
||||
"The name and description of the Card with `card-id`, or nil."
|
||||
[card-id]
|
||||
[card-id :- ::lib.schema.id/card]
|
||||
(t2/select-one [:model/Card :name :description :card_schema], :id card-id))
|
||||
|
||||
(defn table-database-id
|
||||
(mu/defn table-database-id
|
||||
"The Database id of the Table with `table-id`, or nil."
|
||||
[table-id]
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one-fn :db_id :model/Table, :id table-id))
|
||||
|
||||
(defn insert-audit-log!
|
||||
(mu/defn insert-audit-log!
|
||||
"Insert an AuditLog entry."
|
||||
[topic details model-name model-id user-id]
|
||||
[topic :- :keyword
|
||||
details :- [:maybe ::audit-app.schema/audit-log.details]
|
||||
model-name :- [:maybe :string]
|
||||
model-id :- [:maybe ms/PositiveInt]
|
||||
user-id :- [:maybe ::lib.schema.id/user]]
|
||||
(t2/insert! :model/AuditLog
|
||||
:topic topic
|
||||
:details details
|
||||
@@ -44,9 +52,12 @@
|
||||
:model_id model-id
|
||||
:user_id user-id))
|
||||
|
||||
(defn delete-oldest-by-id-subquery!
|
||||
(mu/defn delete-oldest-by-id-subquery!
|
||||
"Delete up to `batch-size` of the `table` rows whose `time-column` is at or before `cutoff`, lowest ids first."
|
||||
[table time-column cutoff batch-size]
|
||||
[table :- :keyword
|
||||
time-column :- :keyword
|
||||
cutoff :- ms/TemporalInstant
|
||||
batch-size :- ms/PositiveInt]
|
||||
(t2/query-one {:delete-from table
|
||||
:where [:in
|
||||
:id
|
||||
@@ -56,9 +67,12 @@
|
||||
:order-by [[:id :asc]]
|
||||
:limit batch-size}]}))
|
||||
|
||||
(defn delete-oldest-with-limit!
|
||||
(mu/defn delete-oldest-with-limit!
|
||||
"Delete up to `batch-size` of the `table` rows whose `time-column` is at or before `cutoff`."
|
||||
[table time-column cutoff batch-size]
|
||||
[table :- :keyword
|
||||
time-column :- :keyword
|
||||
cutoff :- ms/TemporalInstant
|
||||
batch-size :- ms/PositiveInt]
|
||||
(t2/query-one {:delete-from table
|
||||
:where [:<= time-column cutoff]
|
||||
:limit batch-size}))
|
||||
|
||||
33
src/metabase/audit_app/schema.clj
Normal file
33
src/metabase/audit_app/schema.clj
Normal file
@@ -0,0 +1,33 @@
|
||||
(ns metabase.audit-app.schema
|
||||
"Malli schemas for the audit-app module."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
(mr/def ::audit-log.details
|
||||
"The `:details` column of a AuditLog, decoded."
|
||||
:map)
|
||||
|
||||
(mr/def ::audit-log
|
||||
"A AuditLog as selected from the app DB: every column of `:audit_log`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:topic [:or :keyword :string]]
|
||||
[:timestamp ms/TemporalInstant]
|
||||
[:end_timestamp [:maybe ms/TemporalInstant]]
|
||||
[:user_id [:maybe ::lib.schema.id/user]]
|
||||
[:model [:maybe [:or :keyword :string]]]
|
||||
[:model_id [:maybe :int]]
|
||||
[:details ::audit-log.details]])
|
||||
|
||||
(mr/def ::audit-log.update
|
||||
"What an update (or insert) of a AuditLog accepts: every column of `:audit_log` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:topic {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:timestamp {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:end_timestamp {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
|
||||
[:model {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:model_id {:optional true} [:maybe :int]]
|
||||
[:details {:optional true} [:maybe ::audit-log.details]]])
|
||||
@@ -2,100 +2,128 @@
|
||||
"Application database queries for the auth identity module. Every function here is a direct Toucan 2 call with no
|
||||
additional logic, so no other namespace in the module runs a query itself (model definitions still use `toucan2.core`)."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.users.schema :as users.schema]
|
||||
[metabase.util :as u]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defn auth-identity
|
||||
(def ^:private AuthIdentityRow
|
||||
"The writable columns of an AuthIdentity row (excluding `:id`, `:created_at`, `:updated_at`)."
|
||||
[:map {:closed true}
|
||||
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
|
||||
[:provider {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:credentials {:optional true} [:maybe :map]]
|
||||
[:metadata {:optional true} [:maybe :map]]
|
||||
[:provider_id {:optional true} [:maybe :string]]
|
||||
[:last_used_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:expires_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:confirmed_at {:optional true} [:maybe ms/TemporalInstant]]])
|
||||
|
||||
(mu/defn auth-identity
|
||||
"The AuthIdentity of the User with `user-id` at `provider`, or nil."
|
||||
[user-id provider]
|
||||
[user-id :- ::lib.schema.id/user
|
||||
provider :- :string]
|
||||
(t2/select-one :model/AuthIdentity :user_id user-id :provider provider))
|
||||
|
||||
(defn auth-identity-id
|
||||
(mu/defn auth-identity-id
|
||||
"The id of the AuthIdentity of the User with `user-id` at `provider`, or nil."
|
||||
[user-id provider]
|
||||
[user-id :- ::lib.schema.id/user
|
||||
provider :- :string]
|
||||
(t2/select-one-pk :model/AuthIdentity :user_id user-id :provider provider))
|
||||
|
||||
(defn auth-identity-expiry
|
||||
(mu/defn auth-identity-expiry
|
||||
"The id and expiry of the AuthIdentity of the User with `user-id` at `provider`, or nil."
|
||||
[user-id provider]
|
||||
[user-id :- ::lib.schema.id/user
|
||||
provider :- :string]
|
||||
(t2/select-one [:model/AuthIdentity :id :expires_at] :user_id user-id :provider provider))
|
||||
|
||||
(defn auth-identity-exists?
|
||||
(mu/defn auth-identity-exists?
|
||||
"Whether the User with `user-id` has an AuthIdentity at `provider`."
|
||||
[user-id provider]
|
||||
[user-id :- ::lib.schema.id/user
|
||||
provider :- :string]
|
||||
(t2/exists? :model/AuthIdentity :user_id user-id :provider provider))
|
||||
|
||||
(defn insert-auth-identity!
|
||||
"Insert the AuthIdentity `row`."
|
||||
[row]
|
||||
(mu/defn insert-auth-identity!
|
||||
"Insert the AuthIdentity `row`, returning the number inserted."
|
||||
[row :- AuthIdentityRow]
|
||||
(t2/insert! :model/AuthIdentity row))
|
||||
|
||||
(defn update-auth-identity!
|
||||
"Apply `changes` to the AuthIdentity with `id`."
|
||||
[id changes]
|
||||
(mu/defn update-auth-identity!
|
||||
"Apply `changes` to the AuthIdentity with `id`, returning the number updated."
|
||||
[id :- ms/PositiveInt
|
||||
changes :- AuthIdentityRow]
|
||||
(t2/update! :model/AuthIdentity id changes))
|
||||
|
||||
(defn touch-auth-identity!
|
||||
"Set `last_used_at` of the AuthIdentity with `id` to now."
|
||||
[id]
|
||||
(mu/defn touch-auth-identity!
|
||||
"Set `last_used_at` of the AuthIdentity with `id` to now, returning the number updated. `id` may be nil (no
|
||||
matching AuthIdentity), which updates nothing."
|
||||
[id :- [:maybe ms/PositiveInt]]
|
||||
(t2/update! :model/AuthIdentity id {:last_used_at :%now}))
|
||||
|
||||
(defn delete-auth-identities!
|
||||
"Delete the AuthIdentities of the User with `user-id` at `provider`."
|
||||
[user-id provider]
|
||||
(mu/defn delete-auth-identities!
|
||||
"Delete the AuthIdentities of the User with `user-id` at `provider`, returning the number deleted."
|
||||
[user-id :- ::lib.schema.id/user
|
||||
provider :- :string]
|
||||
(t2/delete! :model/AuthIdentity :user_id user-id :provider provider))
|
||||
|
||||
(defn delete-sessions-for-user!
|
||||
"Delete every Session of the User with `user-id`. Duplicates `metabase.session.db/delete-sessions-for-user!`;
|
||||
can't delegate to it because the `session` module already depends on `auth-identity`, so the reverse dependency
|
||||
would be a module cycle."
|
||||
[user-id]
|
||||
(mu/defn delete-sessions-for-user!
|
||||
"Delete every Session of the User with `user-id`, returning the number deleted. Duplicates
|
||||
`metabase.session.db/delete-sessions-for-user!`; can't delegate to it because the `session` module already depends
|
||||
on `auth-identity`, so the reverse dependency would be a module cycle."
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/delete! :model/Session :user_id user-id))
|
||||
|
||||
(defn user
|
||||
(mu/defn user
|
||||
"The User with `user-id`, or nil."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one :model/User user-id))
|
||||
|
||||
(defn user-by-email
|
||||
(mu/defn user-by-email
|
||||
"The User whose email matches `email` case-insensitively, or nil."
|
||||
[email]
|
||||
[email :- :string]
|
||||
(t2/select-one :model/User :%lower.email (u/lower-case-en email)))
|
||||
|
||||
(defn user-login-columns
|
||||
(mu/defn user-login-columns
|
||||
"The id, active flag, last login, and tenant id of the User with `user-id`, or nil."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one [:model/User :id :is_active :last_login :tenant_id] :id user-id))
|
||||
|
||||
(defn user-login-columns-by-email
|
||||
(mu/defn user-login-columns-by-email
|
||||
"The id, active flag, last login, and tenant id of the User whose email matches `email` case-insensitively, or
|
||||
nil."
|
||||
[email]
|
||||
[email :- :string]
|
||||
(t2/select-one [:model/User :id :is_active :last_login :tenant_id] :%lower.email (u/lower-case-en email)))
|
||||
|
||||
(defn user-login-status
|
||||
(mu/defn user-login-status
|
||||
"The id, active flag, and last login of the User with `user-id`, or nil."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one [:model/User :id :is_active :last_login] user-id))
|
||||
|
||||
(defn user-active?
|
||||
(mu/defn user-active?
|
||||
"Whether the User with `user-id` is active."
|
||||
[user-id]
|
||||
[user-id :- ::lib.schema.id/user]
|
||||
(t2/select-one-fn :is_active :model/User :id user-id))
|
||||
|
||||
(defn update-user!
|
||||
"Apply `changes` to the User with `user-id`."
|
||||
[user-id changes]
|
||||
(mu/defn update-user!
|
||||
"Apply `changes` to the User with `user-id`, returning the number updated."
|
||||
[user-id :- ::lib.schema.id/user
|
||||
changes :- ::users.schema/user.update]
|
||||
(t2/update! :model/User user-id changes))
|
||||
|
||||
(defn insert-user-returning-login-columns!
|
||||
(mu/defn insert-user-returning-login-columns!
|
||||
"Insert the User `row` and return its id, last login, active flag, and tenant id."
|
||||
[row]
|
||||
[row :- ::users.schema/user.update]
|
||||
(t2/insert-returning-instance! [:model/User :id :last_login :is_active :tenant_id] row))
|
||||
|
||||
(defn insert-session!
|
||||
(mu/defn insert-session!
|
||||
"Insert a Session and return the inserted instance."
|
||||
[session-id user-id auth-identity-id session-key expires-at]
|
||||
[session-id :- :string
|
||||
user-id :- ::lib.schema.id/user
|
||||
auth-identity-id :- [:maybe ms/PositiveInt]
|
||||
session-key :- :string
|
||||
expires-at :- [:maybe ms/TemporalInstant]]
|
||||
(t2/insert-returning-instance! :model/Session
|
||||
;; Without setting the ID here we can't return an instance on MySQL
|
||||
:id session-id
|
||||
|
||||
43
src/metabase/auth_identity/schema.clj
Normal file
43
src/metabase/auth_identity/schema.clj
Normal file
@@ -0,0 +1,43 @@
|
||||
(ns metabase.auth-identity.schema
|
||||
"Malli schemas for the auth-identity module."
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.malli.schema :as ms]))
|
||||
|
||||
(mr/def ::auth-identity.credentials
|
||||
"The `:credentials` column of a AuthIdentity, decoded."
|
||||
:map)
|
||||
|
||||
(mr/def ::auth-identity.metadata
|
||||
"The `:metadata` column of a AuthIdentity, decoded."
|
||||
:map)
|
||||
|
||||
(mr/def ::auth-identity
|
||||
"A AuthIdentity as selected from the app DB: every column of `:auth_identity`."
|
||||
[:map {:closed true}
|
||||
[:id ms/PositiveInt]
|
||||
[:user_id ::lib.schema.id/user]
|
||||
[:provider [:or :keyword :string]]
|
||||
[:credentials [:maybe ::auth-identity.credentials]]
|
||||
[:metadata [:maybe ::auth-identity.metadata]]
|
||||
[:provider_id [:maybe :string]]
|
||||
[:last_used_at [:maybe ms/TemporalInstant]]
|
||||
[:expires_at [:maybe ms/TemporalInstant]]
|
||||
[:created_at ms/TemporalInstant]
|
||||
[:updated_at ms/TemporalInstant]
|
||||
[:confirmed_at [:maybe ms/TemporalInstant]]])
|
||||
|
||||
(mr/def ::auth-identity.update
|
||||
"What an update (or insert) of a AuthIdentity accepts: every column of `:auth_identity` except `id`, all optional."
|
||||
[:map {:closed true}
|
||||
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
|
||||
[:provider {:optional true} [:maybe [:or :keyword :string]]]
|
||||
[:credentials {:optional true} [:maybe ::auth-identity.credentials]]
|
||||
[:metadata {:optional true} [:maybe ::auth-identity.metadata]]
|
||||
[:provider_id {:optional true} [:maybe :string]]
|
||||
[:last_used_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:expires_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]
|
||||
[:confirmed_at {:optional true} [:maybe ms/TemporalInstant]]])
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user