Merge remote-tracking branch 'origin/v3-ui' into feature/automation-branching-ux

This commit is contained in:
Dean 2024-10-02 09:26:43 +01:00
commit 6d0d4a9b59
18 changed files with 405 additions and 156 deletions

View File

@ -1,3 +1,3 @@
nodejs 20.10.0
python 3.10.0
yarn 1.22.19
yarn 1.22.22

View File

@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "2.32.9",
"version": "2.32.10",
"npmClient": "yarn",
"packages": [
"packages/*",

View File

@ -0,0 +1,29 @@
<script>
export let width
export let height
</script>
<svg
{width}
{height}
viewBox="0 0 13 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.4179 4.13222C9.4179 3.73121 9.26166 3.35428 8.97913 3.07175C8.41342 2.50538 7.4239 2.50408 6.85753 3.07175L5.64342 4.28586C5.6291 4.30018 5.61543 4.3158 5.60305 4.33143C5.58678 4.3438 5.5718 4.35747 5.55683 4.37244L0.491426 9.43785C0.208245 9.72103 0.052002 10.098 0.052002 10.4983C0.052002 10.8987 0.208245 11.2756 0.491426 11.5588C0.774607 11.842 1.15153 11.9982 1.5519 11.9982C1.95227 11.9982 2.32919 11.842 2.61238 11.5588L8.97848 5.1927C9.26166 4.90952 9.4179 4.53259 9.4179 4.13222ZM1.90539 10.8518C1.7166 11.0406 1.3872 11.0406 1.1984 10.8518C1.10401 10.7574 1.05193 10.6318 1.05193 10.4983C1.05193 10.3649 1.104 10.2392 1.1984 10.1448L5.99821 5.34503L6.70845 6.04875L1.90539 10.8518ZM8.2715 4.48571L7.41544 5.34178L6.7052 4.63805L7.56452 3.77873C7.7533 3.58995 8.08271 3.58929 8.2715 3.77939C8.36589 3.87313 8.41798 3.99877 8.41798 4.13223C8.41798 4.26569 8.3659 4.39132 8.2715 4.48571Z"
fill="#C8C8C8"
/>
<path
d="M11.8552 6.55146L11.0144 6.21913L10.879 5.32449C10.8356 5.03919 10.3737 4.98776 10.2686 5.255L9.93606 6.09642L9.04143 6.23085C8.89951 6.25216 8.78884 6.36658 8.77257 6.50947C8.75629 6.65253 8.83783 6.78826 8.97193 6.84148L9.81335 7.17464L9.94794 8.06862C9.9691 8.21053 10.0835 8.32121 10.2266 8.33748C10.3695 8.35375 10.5052 8.27221 10.5586 8.13811L10.8914 7.29751L11.7855 7.1621C11.9283 7.1403 12.0381 7.02637 12.0544 6.88348C12.0707 6.74058 11.9887 6.60403 11.8552 6.55146Z"
fill="#F9634C"
/>
<path
d="M8.94215 1.76145L9.78356 2.0946L9.91815 2.9885C9.93931 3.13049 10.0539 3.24117 10.1968 3.25744C10.3398 3.27371 10.4756 3.19218 10.5288 3.05807L10.8618 2.21739L11.7559 2.08207C11.8985 2.06034 12.0085 1.94633 12.0248 1.80344C12.0411 1.66054 11.959 1.524 11.8254 1.47143L10.9847 1.13909L10.8494 0.244456C10.806 -0.0409246 10.3439 -0.0922745 10.2388 0.174881L9.90643 1.0163L9.0118 1.15089C8.86972 1.17213 8.75905 1.28654 8.74278 1.42952C8.72651 1.57249 8.80804 1.70823 8.94215 1.76145Z"
fill="#8488FD"
/>
<path
d="M3.2379 2.46066L3.92063 2.73091L4.02984 3.45637C4.04709 3.57151 4.14002 3.66135 4.25606 3.67453C4.37194 3.6878 4.48212 3.62163 4.52541 3.51276L4.79557 2.83059L5.52094 2.72074C5.63682 2.70316 5.72601 2.61072 5.73936 2.49468C5.75254 2.37864 5.68597 2.26797 5.57758 2.22533L4.89533 1.95565L4.78548 1.22963C4.75016 0.998038 4.37535 0.956375 4.29007 1.17315L4.0204 1.85597L3.29437 1.96517C3.17915 1.98235 3.08931 2.07527 3.07613 2.19131C3.06294 2.30727 3.12902 2.41737 3.2379 2.46066Z"
fill="#F7D804"
/>
</svg>

View File

@ -67,6 +67,7 @@
"@spectrum-css/vars": "^3.0.1",
"@zerodevx/svelte-json-view": "^1.0.7",
"codemirror": "^5.65.16",
"cron-parser": "^4.9.0",
"dayjs": "^1.10.8",
"downloadjs": "1.4.7",
"fast-json-patch": "^3.1.1",

View File

@ -871,7 +871,7 @@
{:else if value.customType === "cron"}
<CronBuilder
on:change={e => onChange({ [key]: e.detail })}
value={inputData[key]}
cronExpression={inputData[key]}
/>
{:else if value.customType === "automationFields"}
<AutomationSelector

View File

@ -1,41 +1,70 @@
<script>
import { Button, Select, Input, Label } from "@budibase/bbui"
import {
Select,
InlineAlert,
Input,
Label,
Layout,
notifications,
} from "@budibase/bbui"
import { onMount, createEventDispatcher } from "svelte"
import { flags } from "stores/builder"
import { licensing } from "stores/portal"
import { API } from "api"
import MagicWand from "../../../../assets/MagicWand.svelte"
import { helpers, REBOOT_CRON } from "@budibase/shared-core"
const dispatch = createEventDispatcher()
export let value
export let cronExpression
let error
let nextExecutions
// AI prompt
let aiCronPrompt = ""
let loadingAICronExpression = false
$: aiEnabled =
$licensing.customAIConfigsEnabled || $licensing.budibaseAIEnabled
$: {
const exists = CRON_EXPRESSIONS.some(cron => cron.value === value)
const customIndex = CRON_EXPRESSIONS.findIndex(
cron => cron.label === "Custom"
)
if (!exists && customIndex === -1) {
CRON_EXPRESSIONS[0] = { label: "Custom", value: value }
} else if (exists && customIndex !== -1) {
CRON_EXPRESSIONS.splice(customIndex, 1)
if (cronExpression) {
try {
nextExecutions = helpers.cron
.getNextExecutionDates(cronExpression)
.join("\n")
} catch (err) {
nextExecutions = null
}
}
}
const onChange = e => {
if (value !== REBOOT_CRON) {
if (e.detail !== REBOOT_CRON) {
error = helpers.cron.validate(e.detail).err
}
if (e.detail === value || error) {
if (e.detail === cronExpression || error) {
return
}
value = e.detail
cronExpression = e.detail
dispatch("change", e.detail)
}
const updatePreset = e => {
aiCronPrompt = ""
onChange(e)
}
const updateCronExpression = e => {
aiCronPrompt = ""
cronExpression = null
nextExecutions = null
onChange(e)
}
let touched = false
let presets = false
const CRON_EXPRESSIONS = [
{
@ -64,45 +93,130 @@
})
}
})
async function generateAICronExpression() {
loadingAICronExpression = true
try {
const response = await API.generateCronExpression({
prompt: aiCronPrompt,
})
cronExpression = response.message
dispatch("change", response.message)
} catch (err) {
notifications.error(err.message)
} finally {
loadingAICronExpression = false
}
}
</script>
<div class="block-field">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<Layout noPadding gap="S">
<Select
on:change={updatePreset}
value={cronExpression || "Custom"}
secondary
extraThin
label="Use a Preset (Optional)"
options={CRON_EXPRESSIONS}
/>
{#if aiEnabled}
<div class="cron-ai-generator">
<Input
bind:value={aiCronPrompt}
label="Generate Cron Expression with AI"
size="S"
placeholder="Run every hour between 1pm to 4pm everyday of the week"
/>
{#if aiCronPrompt}
<div
class="icon"
class:pulsing-text={loadingAICronExpression}
on:click={generateAICronExpression}
>
<MagicWand height="17" width="17" />
</div>
{/if}
</div>
{/if}
<Input
label="Cron Expression"
{error}
on:change={onChange}
{value}
on:change={updateCronExpression}
value={cronExpression}
on:blur={() => (touched = true)}
updateOnChange={false}
/>
{#if touched && !value}
{#if touched && !cronExpression}
<Label><div class="error">Please specify a CRON expression</div></Label>
{/if}
<div class="presets">
<Button on:click={() => (presets = !presets)}
>{presets ? "Hide" : "Show"} Presets</Button
>
{#if presets}
<Select
on:change={onChange}
value={value || "Custom"}
secondary
extraThin
label="Presets"
options={CRON_EXPRESSIONS}
{#if nextExecutions}
<InlineAlert
type="info"
header="Next Executions"
message={nextExecutions}
/>
{/if}
</div>
</div>
</Layout>
<style>
.presets {
margin-top: var(--spacing-m);
.cron-ai-generator {
flex: 1;
position: relative;
}
.block-field {
padding-top: var(--spacing-s);
.icon {
right: 1px;
bottom: 1px;
position: absolute;
justify-content: center;
align-items: center;
display: flex;
flex-direction: row;
box-sizing: border-box;
border-left: 1px solid var(--spectrum-alias-border-color);
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
border-bottom-right-radius: var(--spectrum-alias-border-radius-regular);
width: 31px;
color: var(--spectrum-alias-text-color);
background-color: var(--spectrum-global-color-gray-75);
transition: background-color
var(--spectrum-global-animation-duration-100, 130ms),
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
border-color var(--spectrum-global-animation-duration-100, 130ms);
height: calc(var(--spectrum-alias-item-height-m) - 2px);
}
.icon:hover {
cursor: pointer;
color: var(--spectrum-alias-text-color-hover);
background-color: var(--spectrum-global-color-gray-50);
border-color: var(--spectrum-alias-border-color-hover);
}
.error {
padding-top: var(--spacing-xs);
color: var(--spectrum-global-color-red-500);
}
.pulsing-text {
font-size: 24px;
font-weight: bold;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% {
opacity: 0.3;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.05);
}
100% {
opacity: 0.3;
transform: scale(1);
}
}
</style>

View File

@ -21,6 +21,7 @@
PROTECTED_EXTERNAL_COLUMNS,
canHaveDefaultColumn,
} from "@budibase/shared-core"
import { makePropSafe } from "@budibase/string-templates"
import { createEventDispatcher, getContext, onMount } from "svelte"
import { cloneDeep } from "lodash/fp"
import { tables, datasources } from "stores/builder"
@ -46,6 +47,7 @@
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
import OptionsEditor from "./OptionsEditor.svelte"
import { isEnabled } from "helpers/featureFlags"
import { getUserBindings } from "dataBinding"
const AUTO_TYPE = FieldType.AUTO
const FORMULA_TYPE = FieldType.FORMULA
@ -191,6 +193,19 @@
fieldId: makeFieldId(t.type, t.subtype),
...t,
}))
$: defaultValueBindings = [
{
type: "context",
runtimeBinding: `${makePropSafe("now")}`,
readableBinding: `Date`,
category: "Date",
icon: "Date",
display: {
name: "Server date",
},
},
...getUserBindings(),
]
const fieldDefinitions = Object.values(FIELDS).reduce(
// Storing the fields by complex field id
@ -781,9 +796,8 @@
setRequired(false)
}
}}
bindings={getBindings({ table })}
bindings={defaultValueBindings}
allowJS
context={rowGoldenSample}
/>
</div>
{/if}

View File

@ -2,7 +2,12 @@
import { getContext } from "svelte"
import CreateEditColumn from "components/backend/DataTable/modals/CreateEditColumn.svelte"
const { datasource } = getContext("grid")
const { datasource, rows } = getContext("grid")
const onUpdate = async () => {
await datasource.actions.refreshDefinition()
await rows.actions.refreshData()
}
</script>
<CreateEditColumn on:updatecolumns={datasource.actions.refreshDefinition} />
<CreateEditColumn on:updatecolumns={onUpdate} />

View File

@ -0,0 +1,11 @@
export const buildAIEndpoints = API => ({
/**
* Generates a cron expression from a prompt
*/
generateCronExpression: async ({ prompt }) => {
return await API.post({
url: "/api/ai/cron",
body: { prompt },
})
},
})

View File

@ -2,6 +2,7 @@ import { Helpers } from "@budibase/bbui"
import { Header } from "@budibase/shared-core"
import { ApiVersion } from "../constants"
import { buildAnalyticsEndpoints } from "./analytics"
import { buildAIEndpoints } from "./ai"
import { buildAppEndpoints } from "./app"
import { buildAttachmentEndpoints } from "./attachments"
import { buildAuthEndpoints } from "./auth"
@ -269,6 +270,7 @@ export const createAPIClient = config => {
// Attach all endpoints
return {
...API,
...buildAIEndpoints(API),
...buildAnalyticsEndpoints(API),
...buildAppEndpoints(API),
...buildAttachmentEndpoints(API),

@ -1 +1 @@
Subproject commit e2fe0f9cc856b4ee1a97df96d623b2d87d4e8733
Subproject commit aca9828117bb97f54f40ee359f1a3f6e259174e7

View File

@ -3,15 +3,9 @@ import {
ViewV2,
SearchRowResponse,
SearchViewRowRequest,
SearchFilterKey,
LogicalOperator,
SearchFilter,
} from "@budibase/types"
import { dataFilters } from "@budibase/shared-core"
import sdk from "../../../sdk"
import { db, context, features } from "@budibase/backend-core"
import { enrichSearchContext } from "./utils"
import { isExternalTableID } from "../../../integrations/utils"
import { context } from "@budibase/backend-core"
export async function searchView(
ctx: UserCtx<SearchViewRowRequest, SearchRowResponse>
@ -28,70 +22,23 @@ export async function searchView(
const { body } = ctx.request
const sqsEnabled = await features.flags.isEnabled("SQS")
const supportsLogicalOperators = isExternalTableID(view.tableId) || sqsEnabled
// Enrich saved query with ephemeral query params.
// We prevent searching on any fields that are saved as part of the query, as
// that could let users find rows they should not be allowed to access.
let query = supportsLogicalOperators
? dataFilters.buildQuery(view.query)
: dataFilters.buildQueryLegacy(view.query)
delete query?.onEmptyFilter
if (body.query) {
// Delete extraneous search params that cannot be overridden
delete body.query.onEmptyFilter
if (!supportsLogicalOperators) {
// In the unlikely event that a Grouped Filter is in a non-SQS environment
// It needs to be ignored entirely
let queryFilters: SearchFilter[] = Array.isArray(view.query)
? view.query
: []
// Extract existing fields
const existingFields =
queryFilters
?.filter(filter => filter.field)
.map(filter => db.removeKeyNumbering(filter.field)) || []
// Carry over filters for unused fields
Object.keys(body.query).forEach(key => {
const operator = key as Exclude<SearchFilterKey, LogicalOperator>
Object.keys(body.query[operator] || {}).forEach(field => {
if (query && !existingFields.includes(db.removeKeyNumbering(field))) {
query[operator]![field] = body.query[operator]![field]
}
})
})
} else {
const conditions = query ? [query] : []
query = {
$and: {
conditions: [...conditions, body.query],
},
}
}
}
await context.ensureSnippetContext(true)
const enrichedQuery = await enrichSearchContext(query || {}, {
user: sdk.users.getUserContextBindings(ctx.user),
})
const result = await sdk.rows.search({
const result = await sdk.rows.search(
{
viewId: view.id,
tableId: view.tableId,
query: enrichedQuery,
query: body.query,
...getSortOptions(body, view),
limit: body.limit,
bookmark: body.bookmark,
paginate: body.paginate,
countRows: body.countRows,
})
},
{
user: sdk.users.getUserContextBindings(ctx.user),
}
)
result.rows.forEach(r => (r._viewId = view.id))
ctx.body = result

View File

@ -33,6 +33,7 @@ import rowActionRoutes from "./rowAction"
export { default as staticRoutes } from "./static"
export { default as publicRoutes } from "./public"
const aiRoutes = pro.ai
const appBackupRoutes = pro.appBackups
const environmentVariableRoutes = pro.environmentVariables
@ -67,6 +68,7 @@ export const mainRoutes: Router[] = [
debugRoutes,
environmentVariableRoutes,
rowActionRoutes,
aiRoutes,
// these need to be handled last as they still use /api/:tableId
// this could be breaking as koa may recognise other routes as this
tableRoutes,

View File

@ -1738,6 +1738,40 @@ describe.each([
})
})
it("views filters are respected even if the column is hidden", async () => {
await config.api.row.save(table._id!, {
one: "foo",
two: "bar",
})
const two = await config.api.row.save(table._id!, {
one: "foo2",
two: "bar2",
})
const view = await config.api.viewV2.create({
tableId: table._id!,
name: generator.guid(),
query: [
{
operator: BasicOperator.EQUAL,
field: "two",
value: "bar2",
},
],
schema: {
id: { visible: true },
one: { visible: false },
two: { visible: false },
},
})
const response = await config.api.viewV2.search(view.id)
expect(response.rows).toHaveLength(1)
expect(response.rows).toEqual([
expect.objectContaining({ _id: two._id }),
])
})
it("views without data can be returned", async () => {
const response = await config.api.viewV2.search(view.id)
expect(response.rows).toHaveLength(0)

View File

@ -1,7 +1,10 @@
import {
EmptyFilterOption,
LogicalOperator,
Row,
RowSearchParams,
SearchFilterKey,
SearchFilters,
SearchResponse,
SortOrder,
Table,
@ -14,9 +17,10 @@ import { ExportRowsParams, ExportRowsResult } from "./search/types"
import { dataFilters } from "@budibase/shared-core"
import sdk from "../../index"
import { searchInputMapping } from "./search/utils"
import { features } from "@budibase/backend-core"
import { db, features } from "@budibase/backend-core"
import tracer from "dd-trace"
import { getQueryableFields, removeInvalidFilters } from "./queryUtils"
import { enrichSearchContext } from "../../../api/controllers/row/utils"
export { isValidFilter } from "../../../integrations/utils"
@ -34,7 +38,8 @@ function pickApi(tableId: any) {
}
export async function search(
options: RowSearchParams
options: RowSearchParams,
context?: Record<string, any>
): Promise<SearchResponse<Row>> {
return await tracer.trace("search", async span => {
span?.addTags({
@ -51,7 +56,73 @@ export async function search(
countRows: options.countRows,
})
options.query = dataFilters.cleanupQuery(options.query || {})
let source: Table | ViewV2
let table: Table
if (options.viewId) {
source = await sdk.views.get(options.viewId)
table = await sdk.views.getTable(source)
options = searchInputMapping(table, options)
} else if (options.tableId) {
source = await sdk.tables.getTable(options.tableId)
table = source
} else {
throw new Error(`Must supply either a view ID or a table ID`)
}
const isExternalTable = isExternalTableID(table._id!)
if (options.query) {
const visibleFields = (
options.fields || Object.keys(table.schema)
).filter(field => table.schema[field].visible !== false)
const queryableFields = await getQueryableFields(table, visibleFields)
options.query = removeInvalidFilters(options.query, queryableFields)
} else {
options.query = {}
}
if (options.viewId) {
const view = await sdk.views.get(options.viewId)
// Enrich saved query with ephemeral query params.
// We prevent searching on any fields that are saved as part of the query, as
// that could let users find rows they should not be allowed to access.
let viewQuery = dataFilters.buildQuery(view.query || [])
if (!isExternalTable && !(await features.flags.isEnabled("SQS"))) {
// Lucene does not accept conditional filters, so we need to keep the old logic
const query: SearchFilters = viewQuery
// Extract existing fields
const existingFields =
view.query
?.filter(filter => filter.field)
.map(filter => db.removeKeyNumbering(filter.field)) || []
// Carry over filters for unused fields
Object.keys(options.query || {}).forEach(key => {
const operator = key as Exclude<SearchFilterKey, LogicalOperator>
Object.keys(options.query[operator] || {}).forEach(field => {
if (!existingFields.includes(db.removeKeyNumbering(field))) {
query[operator]![field] = options.query[operator]![field]
}
})
})
options.query = query
} else {
options.query = {
$and: {
conditions: [viewQuery, options.query],
},
}
}
}
if (context) {
options.query = await enrichSearchContext(options.query, context)
}
options.query = dataFilters.cleanupQuery(options.query)
options.query = dataFilters.fixupFilterArrays(options.query)
span.addTags({
@ -72,30 +143,8 @@ export async function search(
options.sortOrder = options.sortOrder.toLowerCase() as SortOrder
}
let source: Table | ViewV2
let table: Table
if (options.viewId) {
source = await sdk.views.get(options.viewId)
table = await sdk.views.getTable(source)
options = searchInputMapping(table, options)
} else if (options.tableId) {
source = await sdk.tables.getTable(options.tableId)
table = source
options = searchInputMapping(table, options)
} else {
throw new Error(`Must supply either a view ID or a table ID`)
}
if (options.query) {
const visibleFields = (
options.fields || Object.keys(table.schema)
).filter(field => table.schema[field].visible !== false)
const queryableFields = await getQueryableFields(table, visibleFields)
options.query = removeInvalidFilters(options.query, queryableFields)
}
const isExternalTable = isExternalTableID(table._id!)
let result: SearchResponse<Row>
if (isExternalTable) {
span?.addTags({ searchType: "external" })

View File

@ -130,6 +130,26 @@ export function getUserContextBindings(user: ContextUser) {
return {}
}
// Current user context for bindable search
const { _id, _rev, firstName, lastName, email, status, roleId } = user
return { _id, _rev, firstName, lastName, email, status, roleId }
const {
_id,
_rev,
firstName,
lastName,
email,
status,
roleId,
globalId,
userId,
} = user
return {
_id,
_rev,
firstName,
lastName,
email,
status,
roleId,
globalId,
userId,
}
}

View File

@ -1,4 +1,5 @@
import cronValidate from "cron-validate"
import cronParser from "cron-parser"
const INPUT_CRON_START = "(Input cron: "
const ERROR_SWAPS = {
@ -30,6 +31,19 @@ function improveErrors(errors: string[]): string[] {
return finalErrors
}
export function getNextExecutionDates(
cronExpression: string,
limit: number = 4
): string[] {
const parsed = cronParser.parseExpression(cronExpression)
const nextRuns = []
for (let i = 0; i < limit; i++) {
nextRuns.push(parsed.next().toString())
}
return nextRuns
}
export function validate(
cronExpression: string
): { valid: false; err: string[] } | { valid: true } {

View File

@ -817,23 +817,23 @@
tslib "^2.2.0"
"@azure/msal-browser@^3.11.1":
version "3.23.0"
resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.23.0.tgz#446aaf268247e5943f464f007d3aa3a04abfe95b"
integrity sha512-+QgdMvaeEpdtgRTD7AHHq9aw8uga7mXVHV1KshO1RQ2uI5B55xJ4aEpGlg/ga3H+0arEVcRfT4ZVmX7QLXiCVw==
version "3.24.0"
resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.24.0.tgz#3208047672d0b0c943b0bef5f995d510d6582ae4"
integrity sha512-JGNV9hTYAa7lsum9IMIibn2kKczAojNihGo1hi7pG0kNrcKej530Fl6jxwM05A44/6I079CSn6WxYxbVhKUmWg==
dependencies:
"@azure/msal-common" "14.14.2"
"@azure/msal-common" "14.15.0"
"@azure/msal-common@14.14.2":
version "14.14.2"
resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.14.2.tgz#583b4ac9c089953718d7a5e2f3b8df2d4dbb17f4"
integrity sha512-XV0P5kSNwDwCA/SjIxTe9mEAsKB0NqGNSuaVrkCCE2lAyBr/D6YtD80Vkdp4tjWnPFwjzkwldjr1xU/facOJog==
"@azure/msal-common@14.15.0":
version "14.15.0"
resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.15.0.tgz#0e27ac0bb88fe100f4f8d1605b64d5c268636a55"
integrity sha512-ImAQHxmpMneJ/4S8BRFhjt1MZ3bppmpRPYYNyzeQPeFN288YKbb8TmmISQEbtfkQ1BPASvYZU5doIZOPBAqENQ==
"@azure/msal-node@^2.9.2":
version "2.13.1"
resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.13.1.tgz#f144371275b7c3cbe564762b84772a9732457a47"
integrity sha512-sijfzPNorKt6+9g1/miHwhj6Iapff4mPQx1azmmZExgzUROqWTM1o3ACyxDja0g47VpowFy/sxTM/WsuCyXTiw==
version "2.14.0"
resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.14.0.tgz#7881895d41b03d8b9b38a29550ba3bbb15f73b3c"
integrity sha512-rrfzIpG3Q1rHjVYZmHAEDidWAZZ2cgkxlIcMQ8dHebRISaZ2KCV33Q8Vs+uaV6lxweROabNxKFlR2lIKagZqYg==
dependencies:
"@azure/msal-common" "14.14.2"
"@azure/msal-common" "14.15.0"
jsonwebtoken "^9.0.0"
uuid "^8.3.0"
@ -9167,6 +9167,13 @@ cron-parser@^4.2.1:
dependencies:
luxon "^3.2.1"
cron-parser@^4.9.0:
version "4.9.0"
resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.9.0.tgz#0340694af3e46a0894978c6f52a6dbb5c0f11ad5"
integrity sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==
dependencies:
luxon "^3.2.1"
cron-validate@1.4.5:
version "1.4.5"
resolved "https://registry.yarnpkg.com/cron-validate/-/cron-validate-1.4.5.tgz#eceb221f7558e6302e5f84c7b3a454fdf4d064c3"