dineway 0.1.34 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -4
- package/dist/api/route-utils.d.mts +2 -2
- package/dist/api/schemas/index.d.mts +1 -1
- package/dist/{api-BaFOFZnE.mjs → api-K0U9SYx7.mjs} +1 -1
- package/dist/astro/index.d.mts +2 -2
- package/dist/astro/index.mjs +1 -1
- package/dist/astro/middleware/auth.d.mts +2 -2
- package/dist/astro/middleware/seed.mjs +1 -1
- package/dist/astro/middleware.mjs +4 -4
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +1 -1
- package/dist/astro/routes/api/admin/plugins/index.mjs +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +1 -1
- package/dist/astro/routes/api/admin/plugins/updates.mjs +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +1 -1
- package/dist/astro/routes/api/auth/dev-bypass.mjs +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +1 -1
- package/dist/astro/routes/api/health.mjs +1 -1
- package/dist/astro/routes/api/manifest.mjs +1 -1
- package/dist/astro/routes/api/mcp.mjs +1 -1
- package/dist/astro/routes/api/openapi.json.mjs +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +1 -1
- package/dist/astro/routes/api/schema/collections/index.mjs +1 -1
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +1 -1
- package/dist/astro/routes/api/schema/orphans/index.mjs +1 -1
- package/dist/astro/routes/api/setup/dev-bypass.mjs +1 -1
- package/dist/astro/routes/api/setup/index.mjs +1 -1
- package/dist/astro/routes/api/well-known/auth.mjs +1 -1
- package/dist/astro/types.d.mts +2 -2
- package/dist/{bylines-CeNpt_vk.d.mts → bylines-C8EfvtqH.d.mts} +25 -25
- package/dist/cli/index.mjs +532 -114
- package/dist/db/index.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -3
- package/dist/media/local-runtime.d.mts +2 -2
- package/dist/plugins/adapt-sandbox-entry.d.mts +2 -2
- package/dist/{preview-5HuX6fjF.mjs → preview-BhgxNRWI.mjs} +1 -1
- package/dist/{runner-lqEiJbO-.mjs → runner-S3smkgdc.mjs} +15 -2
- package/dist/{runtime-CP8eY2L-.d.mts → runtime-BM9sqnzO.d.mts} +2 -2
- package/dist/runtime.d.mts +2 -2
- package/dist/version-BCYrkQqz.mjs +6 -0
- package/package.json +1 -1
- package/dist/version-HcqOJZFv.mjs +0 -6
package/dist/cli/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { t as __exportAll } from "../chunk-ClPoSABd.mjs";
|
|
3
3
|
import { n as createDatabase } from "../connection-BCNICDWN.mjs";
|
|
4
4
|
import { c as listTablesLike } from "../dialect-helpers-TkdbkFad.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { i as runMigrations, n as getMigrationStatus, t as getMigrationFingerprint } from "../runner-S3smkgdc.mjs";
|
|
6
6
|
import { r as isI18nEnabled } from "../config-XW5tMrH8.mjs";
|
|
7
7
|
import { n as slugify } from "../slugify-BzGxlOFx.mjs";
|
|
8
8
|
import { t as ContentRepository } from "../content-DvpMad_N.mjs";
|
|
@@ -27,9 +27,9 @@ import { createHeaderAwareFetch, customHeadersInterceptor, isRedirectResponse, r
|
|
|
27
27
|
import { o as convertDataForRead } from "../transport-B7kO-4ee.mjs";
|
|
28
28
|
import { DinewayClient } from "../client/index.mjs";
|
|
29
29
|
import { LocalStorage } from "../storage/local.mjs";
|
|
30
|
+
import { createHash } from "node:crypto";
|
|
30
31
|
import { imageSize } from "image-size";
|
|
31
32
|
import { createGzipDecoder, unpackTar } from "modern-tar";
|
|
32
|
-
import { createHash } from "node:crypto";
|
|
33
33
|
import { createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
34
34
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
35
35
|
import { defineCommand, runCommand, runMain } from "citty";
|
|
@@ -190,7 +190,7 @@ function removeMarketplaceCredential(registryUrl) {
|
|
|
190
190
|
//#region src/cli/project-env.ts
|
|
191
191
|
const DEFAULT_DINEWAY_URL = "http://localhost:4321";
|
|
192
192
|
const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
193
|
-
const LINE_SPLIT_PATTERN = /\r?\n/;
|
|
193
|
+
const LINE_SPLIT_PATTERN$1 = /\r?\n/;
|
|
194
194
|
const NEWLINE_PATTERN = /\r?\n/g;
|
|
195
195
|
function parseDotenvValue(value) {
|
|
196
196
|
const trimmed = value.trim();
|
|
@@ -199,7 +199,7 @@ function parseDotenvValue(value) {
|
|
|
199
199
|
}
|
|
200
200
|
function parseDotenv(text) {
|
|
201
201
|
const env = {};
|
|
202
|
-
for (const line of text.split(LINE_SPLIT_PATTERN)) {
|
|
202
|
+
for (const line of text.split(LINE_SPLIT_PATTERN$1)) {
|
|
203
203
|
const trimmed = line.trim();
|
|
204
204
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
205
205
|
const normalized = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
|
|
@@ -233,7 +233,7 @@ function formatEnvValue(value) {
|
|
|
233
233
|
async function upsertProjectEnv(cwd, values) {
|
|
234
234
|
const envPath = join(cwd, ".env");
|
|
235
235
|
const existing = await readFile(envPath, "utf-8").catch(() => "");
|
|
236
|
-
const lines = existing ? existing.split(LINE_SPLIT_PATTERN) : [];
|
|
236
|
+
const lines = existing ? existing.split(LINE_SPLIT_PATTERN$1) : [];
|
|
237
237
|
const seen = /* @__PURE__ */ new Set();
|
|
238
238
|
const nextLines = lines.map((line) => {
|
|
239
239
|
const trimmed = line.trim();
|
|
@@ -255,7 +255,7 @@ async function upsertProjectEnv(cwd, values) {
|
|
|
255
255
|
async function ensureProjectGitignoreEntry(cwd, entry) {
|
|
256
256
|
const gitignorePath = join(cwd, ".gitignore");
|
|
257
257
|
const existing = await readFile(gitignorePath, "utf-8").catch(() => "");
|
|
258
|
-
if (existing.split(LINE_SPLIT_PATTERN).map((line) => line.trim()).includes(entry)) return;
|
|
258
|
+
if (existing.split(LINE_SPLIT_PATTERN$1).map((line) => line.trim()).includes(entry)) return;
|
|
259
259
|
await writeFile(gitignorePath, `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${entry}\n`, "utf-8");
|
|
260
260
|
}
|
|
261
261
|
|
|
@@ -1637,7 +1637,7 @@ async function writeForgewayCredentials(credentials, cwd) {
|
|
|
1637
1637
|
await writeStore(store, cwd);
|
|
1638
1638
|
}
|
|
1639
1639
|
function shadowGrantKey(platformApiUrl, placeId) {
|
|
1640
|
-
return `${platformApiUrl.replace(TRAILING_SLASH_PATTERN$1, "")}#${placeId}`;
|
|
1640
|
+
return `${platformApiUrl.replace(TRAILING_SLASH_PATTERN$1, "")}#${placeId ?? "__unbound__"}`;
|
|
1641
1641
|
}
|
|
1642
1642
|
async function readForgewayShadowGrant(platformApiUrl, placeId, cwd) {
|
|
1643
1643
|
const store = await readStore(cwd);
|
|
@@ -1665,23 +1665,37 @@ const DATABASE_ENV_VAR_NAMES = ["DINEWAY_DATABASE_URL", "DINEWAY_DATABASE_AUTH_T
|
|
|
1665
1665
|
const POLL_INTERVAL_MS$1 = 5e3;
|
|
1666
1666
|
const POLL_TIMEOUT_MS$1 = 3e5;
|
|
1667
1667
|
const DIRECT_UPLOAD_CONCURRENCY = 8;
|
|
1668
|
-
const
|
|
1668
|
+
const FILE_HASH_CONCURRENCY = 8;
|
|
1669
1669
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
1670
1670
|
const PLACE_ID_PATTERN = /^[A-Za-z0-9_-]{10,}$/;
|
|
1671
1671
|
const SLUG_SEPARATOR_PATTERN = /[^a-z0-9]+/g;
|
|
1672
1672
|
const LEADING_TRAILING_HYPHEN_PATTERN = /^-+|-+$/g;
|
|
1673
|
+
const DIACRITICS_PATTERN = /[\u0300-\u036f]/g;
|
|
1674
|
+
const NON_COMPARE_PATTERN = /[^\p{Letter}\p{Number}]+/gu;
|
|
1675
|
+
const LOCATION_PARTS_PATTERN = /[,;|/]+|\s+-\s+/u;
|
|
1673
1676
|
const JWT_PATTERN = /[A-Za-z0-9_-]{32,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g;
|
|
1674
1677
|
const TOKEN_ASSIGNMENT_PATTERN = /(DINEWAY_DATABASE_AUTH_TOKEN|DATABASE_AUTH_TOKEN|AUTH_TOKEN)=\S+/gi;
|
|
1675
1678
|
const TRAILING_SLASH_PATTERN = /\/$/;
|
|
1679
|
+
const LEADING_SLASHES_PATTERN = /^\/+/;
|
|
1680
|
+
const TRAILING_SLASHES_PATTERN = /\/+$/;
|
|
1681
|
+
const REGEXP_ESCAPE_PATTERN = /[|\\{}()[\]^$+?.]/g;
|
|
1682
|
+
const LINE_SPLIT_PATTERN = /\r?\n/;
|
|
1676
1683
|
const BACKSLASH_PATTERN = /\\/g;
|
|
1677
|
-
const DIACRITICS_PATTERN = /[\u0300-\u036f]/g;
|
|
1678
|
-
const NON_COMPARE_PATTERN = /[^\p{Letter}\p{Number}]+/gu;
|
|
1679
|
-
const LOCATION_PARTS_PATTERN = /[,;|/]+|\s+-\s+/u;
|
|
1680
1684
|
const SHADOW_EMAIL_PATTERN = /^shadow_[a-f0-9]{16}@dineway\.ai$/i;
|
|
1685
|
+
const FOOD_TYPES = new Set([
|
|
1686
|
+
"restaurant",
|
|
1687
|
+
"cafe",
|
|
1688
|
+
"food",
|
|
1689
|
+
"meal_takeaway",
|
|
1690
|
+
"meal_delivery",
|
|
1691
|
+
"bakery"
|
|
1692
|
+
]);
|
|
1681
1693
|
const EXCLUDE_PATTERNS = [
|
|
1682
1694
|
"node_modules",
|
|
1683
1695
|
".git",
|
|
1684
1696
|
".next",
|
|
1697
|
+
".astro",
|
|
1698
|
+
".plan",
|
|
1685
1699
|
".env",
|
|
1686
1700
|
".env.local",
|
|
1687
1701
|
"dist",
|
|
@@ -1703,7 +1717,11 @@ const EXCLUDE_PATTERNS = [
|
|
|
1703
1717
|
".turbo",
|
|
1704
1718
|
".cache",
|
|
1705
1719
|
"skills",
|
|
1706
|
-
"coverage"
|
|
1720
|
+
"coverage",
|
|
1721
|
+
"uploads",
|
|
1722
|
+
"data.db",
|
|
1723
|
+
"data.db-shm",
|
|
1724
|
+
"data.db-wal"
|
|
1707
1725
|
];
|
|
1708
1726
|
var ForgewayApiError = class extends Error {
|
|
1709
1727
|
constructor(message, status) {
|
|
@@ -1739,6 +1757,39 @@ function getEnv(name) {
|
|
|
1739
1757
|
const value = process.env[name];
|
|
1740
1758
|
return value && value.trim() ? value.trim() : void 0;
|
|
1741
1759
|
}
|
|
1760
|
+
function isVerboseDeploy(options) {
|
|
1761
|
+
return options?.verbose === true || getEnv("DINEWAY_DEPLOY_VERBOSE") === "1";
|
|
1762
|
+
}
|
|
1763
|
+
function formatDuration(ms) {
|
|
1764
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
1765
|
+
return `${Math.round(ms / 100) / 10}s`;
|
|
1766
|
+
}
|
|
1767
|
+
function formatBytes$1(bytes) {
|
|
1768
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1769
|
+
const units = [
|
|
1770
|
+
"KB",
|
|
1771
|
+
"MB",
|
|
1772
|
+
"GB"
|
|
1773
|
+
];
|
|
1774
|
+
let value = bytes / 1024;
|
|
1775
|
+
for (let index = 0; index < units.length; index++) {
|
|
1776
|
+
const unit = units[index];
|
|
1777
|
+
if (value < 1024 || index === units.length - 1) return `${value.toFixed(value >= 10 ? 0 : 1)} ${unit}`;
|
|
1778
|
+
value /= 1024;
|
|
1779
|
+
}
|
|
1780
|
+
return `${bytes} B`;
|
|
1781
|
+
}
|
|
1782
|
+
function deploymentFileSize(files) {
|
|
1783
|
+
return files.reduce((total, file) => total + file.size, 0);
|
|
1784
|
+
}
|
|
1785
|
+
function formatSeedSummary(seed) {
|
|
1786
|
+
if (!seed) return "no seed";
|
|
1787
|
+
return [
|
|
1788
|
+
`collections ${seed.collections.created}/${seed.collections.skipped}/${seed.collections.updated}`,
|
|
1789
|
+
`content ${seed.content.created}/${seed.content.skipped}/${seed.content.updated}`,
|
|
1790
|
+
`media ${seed.media.created}/${seed.media.skipped}`
|
|
1791
|
+
].join(", ");
|
|
1792
|
+
}
|
|
1742
1793
|
async function readJsonResponse(response) {
|
|
1743
1794
|
return await response.json().catch(() => null);
|
|
1744
1795
|
}
|
|
@@ -1792,7 +1843,7 @@ async function refreshFormalAccessToken(context, deps) {
|
|
|
1792
1843
|
return payload.accessToken;
|
|
1793
1844
|
}
|
|
1794
1845
|
async function refreshShadowAccessToken(context, deps) {
|
|
1795
|
-
if (!context.refreshToken
|
|
1846
|
+
if (!context.refreshToken) throw new Error("Dineway shadow deploy grant is missing its refresh token.");
|
|
1796
1847
|
const response = await forgewayRequest(`${context.platformApiUrl}/api/auth/refresh?client_type=server`, {
|
|
1797
1848
|
method: "POST",
|
|
1798
1849
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1849,7 +1900,6 @@ function isStoredFormalCredentialForGrant(stored, grant, email) {
|
|
|
1849
1900
|
if (normalizeEmail(stored.user.email) !== email) return false;
|
|
1850
1901
|
if (stored.user.emailVerified !== true) return false;
|
|
1851
1902
|
if (isShadowEmail(stored.user.email)) return false;
|
|
1852
|
-
if (grant.user?.id && stored.user.id !== grant.user.id) return false;
|
|
1853
1903
|
return true;
|
|
1854
1904
|
}
|
|
1855
1905
|
function assertVerifiedFormalAccountUser(user, expectedEmail) {
|
|
@@ -1917,7 +1967,7 @@ async function resolveShadowProjectContext(cwd, options, deps) {
|
|
|
1917
1967
|
const pkg = await readDeployPackageJson(cwd).catch(() => null);
|
|
1918
1968
|
const saved = pkg ? getSavedForgewayMetadata(pkg) : {};
|
|
1919
1969
|
const platformApiUrl = normalizePlatformApiUrl(getEnv("DINEWAY_API_BASE_URL") ?? getEnv("FORGEWAY_API_URL") ?? (typeof saved.platformApiUrl === "string" ? saved.platformApiUrl : void 0));
|
|
1920
|
-
const savedPlaceId = getEnv("DINEWAY_PLACE_ID") ?? (typeof saved.placeId === "string" ? saved.placeId : void 0);
|
|
1970
|
+
const savedPlaceId = options.placeId ?? getEnv("DINEWAY_PLACE_ID") ?? (typeof saved.placeId === "string" ? saved.placeId : void 0);
|
|
1921
1971
|
let grant = await (deps.readShadowGrant ?? readForgewayShadowGrant)(platformApiUrl, savedPlaceId, cwd);
|
|
1922
1972
|
let restaurant;
|
|
1923
1973
|
if (!grant) {
|
|
@@ -1936,6 +1986,28 @@ async function resolveShadowProjectContext(cwd, options, deps) {
|
|
|
1936
1986
|
name: options.restaurantName ?? grant.restaurantName,
|
|
1937
1987
|
city: options.city ?? grant.city
|
|
1938
1988
|
};
|
|
1989
|
+
if (!grant.placeId) {
|
|
1990
|
+
const shadowRestaurant = restaurant?.name && restaurant.city ? {
|
|
1991
|
+
name: restaurant.name,
|
|
1992
|
+
city: restaurant.city
|
|
1993
|
+
} : await resolveShadowRestaurantInfo(options, deps);
|
|
1994
|
+
restaurant = shadowRestaurant;
|
|
1995
|
+
grant = savedPlaceId ? await bindShadowGrantPlace({
|
|
1996
|
+
platformApiUrl,
|
|
1997
|
+
grant,
|
|
1998
|
+
placeId: savedPlaceId,
|
|
1999
|
+
restaurant: shadowRestaurant,
|
|
2000
|
+
cwd,
|
|
2001
|
+
deps
|
|
2002
|
+
}) : await resolvePlaceIdWithShadowSearch({
|
|
2003
|
+
platformApiUrl,
|
|
2004
|
+
grant,
|
|
2005
|
+
restaurant: shadowRestaurant,
|
|
2006
|
+
cwd,
|
|
2007
|
+
deps
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
if (!grant.placeId) throw new Error("Could not resolve a Dineway place id for Forgeway deploy.");
|
|
1939
2011
|
const shadowContext = {
|
|
1940
2012
|
projectDir: cwd,
|
|
1941
2013
|
platformApiUrl,
|
|
@@ -1965,107 +2037,280 @@ async function resolveShadowProjectContext(cwd, options, deps) {
|
|
|
1965
2037
|
async function resolveProjectContext(cwd, options, deps) {
|
|
1966
2038
|
return await resolveShadowProjectContext(cwd, options, deps);
|
|
1967
2039
|
}
|
|
2040
|
+
async function createShadowUser(platformApiUrl, placeId, deps) {
|
|
2041
|
+
const response = await forgewayRequest(`${platformApiUrl}/api/auth/users/shadow`, {
|
|
2042
|
+
method: "POST",
|
|
2043
|
+
headers: { "Content-Type": "application/json" },
|
|
2044
|
+
body: JSON.stringify({
|
|
2045
|
+
...placeId ? { placeId } : {},
|
|
2046
|
+
language: "en"
|
|
2047
|
+
})
|
|
2048
|
+
}, deps);
|
|
2049
|
+
if (!response.ok) throw new Error(await parseErrorResponse(response));
|
|
2050
|
+
const payload = await response.json();
|
|
2051
|
+
if (!payload.accessToken || !payload.refreshToken) throw new Error("Dineway shadow user response was missing tokens.");
|
|
2052
|
+
return payload;
|
|
2053
|
+
}
|
|
1968
2054
|
function isRecord(value) {
|
|
1969
|
-
return typeof value === "object" && value !== null;
|
|
2055
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2056
|
+
}
|
|
2057
|
+
function uniqueStrings(values) {
|
|
2058
|
+
return [...new Set(values.map((value) => String(value || "").trim()).filter(Boolean))];
|
|
1970
2059
|
}
|
|
1971
|
-
function
|
|
2060
|
+
function displayText(value) {
|
|
1972
2061
|
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
1973
|
-
if (Array.isArray(value)) return value.map(
|
|
1974
|
-
if (isRecord(value)) return
|
|
2062
|
+
if (Array.isArray(value)) return value.map(displayText).filter(Boolean).join(" ");
|
|
2063
|
+
if (isRecord(value)) return displayText(value.text) || displayText(value.name) || displayText(value.displayName) || displayText(value.longText) || displayText(value.shortText) || displayText(value.long_name) || displayText(value.short_name) || displayText(value.value);
|
|
1975
2064
|
return "";
|
|
1976
2065
|
}
|
|
1977
|
-
function
|
|
1978
|
-
return value.toLowerCase().normalize("NFKD").replace(DIACRITICS_PATTERN, "").replace(NON_COMPARE_PATTERN, "");
|
|
2066
|
+
function normalizeCompare(value) {
|
|
2067
|
+
return String(value || "").toLowerCase().normalize("NFKD").replace(DIACRITICS_PATTERN, "").replace(NON_COMPARE_PATTERN, "");
|
|
1979
2068
|
}
|
|
1980
2069
|
function candidatePlaceId(candidate) {
|
|
1981
2070
|
return String(candidate.placeId || candidate.id || "").trim();
|
|
1982
2071
|
}
|
|
1983
|
-
function
|
|
1984
|
-
return [
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
]
|
|
2072
|
+
function candidateNameTexts(candidate) {
|
|
2073
|
+
return uniqueStrings([
|
|
2074
|
+
displayText(candidate.displayName),
|
|
2075
|
+
displayText(candidate.name),
|
|
2076
|
+
displayText(candidate.businessName),
|
|
2077
|
+
displayText(candidate.title)
|
|
2078
|
+
]);
|
|
2079
|
+
}
|
|
2080
|
+
function candidateAddressTexts(candidate) {
|
|
2081
|
+
const addressComponents = Array.isArray(candidate.addressComponents) ? candidate.addressComponents.flatMap((component) => isRecord(component) ? [
|
|
2082
|
+
displayText(component.longText),
|
|
2083
|
+
displayText(component.shortText),
|
|
2084
|
+
displayText(component.long_name),
|
|
2085
|
+
displayText(component.short_name),
|
|
2086
|
+
displayText(component.name)
|
|
2087
|
+
] : []) : [];
|
|
2088
|
+
return uniqueStrings([
|
|
2089
|
+
displayText(candidate.formattedAddress),
|
|
2090
|
+
displayText(candidate.shortFormattedAddress),
|
|
2091
|
+
displayText(candidate.address),
|
|
2092
|
+
displayText(candidate.vicinity),
|
|
2093
|
+
displayText(candidate.plusCode?.compoundCode),
|
|
2094
|
+
displayText(candidate.plus_code?.compound_code),
|
|
2095
|
+
...addressComponents
|
|
2096
|
+
]);
|
|
2097
|
+
}
|
|
2098
|
+
function candidateTypes(candidate) {
|
|
2099
|
+
return uniqueStrings([
|
|
2100
|
+
...Array.isArray(candidate.types) ? candidate.types : [],
|
|
2101
|
+
candidate.primaryType,
|
|
2102
|
+
candidate.primary_type
|
|
2103
|
+
]).map((type) => type.toLowerCase());
|
|
2104
|
+
}
|
|
2105
|
+
function locationParts(value) {
|
|
2106
|
+
return uniqueStrings(value.split(LOCATION_PARTS_PATTERN).map((part) => normalizeCompare(part)).filter(Boolean));
|
|
2107
|
+
}
|
|
2108
|
+
function scoreNameMatch(queryName, candidateNames) {
|
|
2109
|
+
let bestScore = 0;
|
|
2110
|
+
let bestReason = "";
|
|
2111
|
+
for (const name of candidateNames) {
|
|
2112
|
+
const currentName = normalizeCompare(name);
|
|
2113
|
+
if (!queryName || !currentName) continue;
|
|
2114
|
+
let score = 0;
|
|
2115
|
+
let reason = "";
|
|
2116
|
+
if (currentName === queryName) {
|
|
2117
|
+
score = 120;
|
|
2118
|
+
reason = "exact_name";
|
|
2119
|
+
} else if (currentName.includes(queryName)) {
|
|
2120
|
+
score = 80;
|
|
2121
|
+
reason = "candidate_contains_name";
|
|
2122
|
+
} else if (queryName.includes(currentName) && currentName.length >= Math.min(4, queryName.length)) {
|
|
2123
|
+
score = 45;
|
|
2124
|
+
reason = "query_contains_candidate_name";
|
|
2125
|
+
}
|
|
2126
|
+
if (score > bestScore) {
|
|
2127
|
+
bestScore = score;
|
|
2128
|
+
bestReason = reason;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
return {
|
|
2132
|
+
score: bestScore,
|
|
2133
|
+
reason: bestReason
|
|
2134
|
+
};
|
|
1990
2135
|
}
|
|
1991
|
-
function
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2136
|
+
function scoreLocationMatch(city, candidate) {
|
|
2137
|
+
const queryLocation = normalizeCompare(city);
|
|
2138
|
+
const parts = locationParts(city);
|
|
2139
|
+
const candidateLocation = normalizeCompare(candidateAddressTexts(candidate).join(" "));
|
|
2140
|
+
let score = 0;
|
|
2141
|
+
const reasons = [];
|
|
2142
|
+
if (!candidateLocation) return {
|
|
2143
|
+
score,
|
|
2144
|
+
reasons
|
|
2145
|
+
};
|
|
2146
|
+
if (queryLocation && candidateLocation.includes(queryLocation)) return {
|
|
2147
|
+
score: 55,
|
|
2148
|
+
reasons: ["location_full_match"]
|
|
2149
|
+
};
|
|
2150
|
+
const [primaryPart, ...secondaryParts] = parts;
|
|
2151
|
+
if (primaryPart && candidateLocation.includes(primaryPart)) {
|
|
2152
|
+
score += 38;
|
|
2153
|
+
reasons.push("location_primary_match");
|
|
2154
|
+
}
|
|
2155
|
+
for (const part of secondaryParts) if (part.length >= 3 && candidateLocation.includes(part)) {
|
|
2156
|
+
score += 10;
|
|
2157
|
+
reasons.push("location_secondary_match");
|
|
2158
|
+
}
|
|
2159
|
+
return {
|
|
2160
|
+
score,
|
|
2161
|
+
reasons
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
function scorePlaceCandidate(candidate, context) {
|
|
2165
|
+
const queryName = normalizeCompare(context.name);
|
|
2166
|
+
const currentPlaceId = candidatePlaceId(candidate);
|
|
2167
|
+
let score = 0;
|
|
2168
|
+
const reasons = [];
|
|
2169
|
+
if (context.providedPlaceId && currentPlaceId === context.providedPlaceId) {
|
|
2170
|
+
score += 1e4;
|
|
2171
|
+
reasons.push("provided_place_id");
|
|
2172
|
+
}
|
|
2173
|
+
const nameMatch = scoreNameMatch(queryName, candidateNameTexts(candidate));
|
|
2174
|
+
if (nameMatch.score) {
|
|
2175
|
+
score += nameMatch.score;
|
|
2176
|
+
reasons.push(nameMatch.reason);
|
|
2177
|
+
}
|
|
2178
|
+
const locationMatch = scoreLocationMatch(context.city, candidate);
|
|
2179
|
+
if (locationMatch.score) {
|
|
2180
|
+
score += locationMatch.score;
|
|
2181
|
+
reasons.push(...locationMatch.reasons);
|
|
2182
|
+
}
|
|
2183
|
+
if (candidateTypes(candidate).some((type) => FOOD_TYPES.has(type))) {
|
|
2184
|
+
score += 10;
|
|
2185
|
+
reasons.push("food_type");
|
|
2186
|
+
}
|
|
2187
|
+
const businessStatus = String(candidate.businessStatus ?? candidate.business_status ?? "").toUpperCase();
|
|
2188
|
+
if (businessStatus === "OPERATIONAL") {
|
|
2189
|
+
score += 4;
|
|
2190
|
+
reasons.push("operational");
|
|
2191
|
+
} else if (businessStatus === "CLOSED_PERMANENTLY") {
|
|
2192
|
+
score -= 60;
|
|
2193
|
+
reasons.push("closed_permanently");
|
|
2194
|
+
}
|
|
2195
|
+
const rating = Number(candidate.rating);
|
|
2196
|
+
if (Number.isFinite(rating)) score += Math.min(Math.max(rating, 0), 5);
|
|
2197
|
+
const reviewCount = Number(candidate.userRatingCount ?? candidate.user_ratings_total);
|
|
2198
|
+
if (Number.isFinite(reviewCount)) score += Math.min(Math.max(reviewCount, 0), 500) / 100;
|
|
2199
|
+
return {
|
|
2200
|
+
score,
|
|
2201
|
+
reason: reasons.join("+") || "fallback"
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
function selectPlaceCandidate(candidates, context) {
|
|
2004
2205
|
let best = null;
|
|
2206
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
2207
|
+
let bestReason = "fallback";
|
|
2005
2208
|
for (const candidate of candidates) {
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2209
|
+
const { score, reason } = scorePlaceCandidate(candidate, context);
|
|
2210
|
+
if (score > bestScore) {
|
|
2211
|
+
best = candidate;
|
|
2212
|
+
bestScore = score;
|
|
2213
|
+
bestReason = reason;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
return best ? {
|
|
2217
|
+
candidate: best,
|
|
2218
|
+
score: bestScore,
|
|
2219
|
+
reason: bestReason
|
|
2220
|
+
} : null;
|
|
2221
|
+
}
|
|
2222
|
+
function extractPlaceCandidates(payload) {
|
|
2223
|
+
const root = isRecord(payload) ? payload : {};
|
|
2224
|
+
const nestedData = isRecord(root.data) ? root.data : {};
|
|
2225
|
+
const places = root.places ?? root.results ?? nestedData.places ?? nestedData.results;
|
|
2226
|
+
if (!Array.isArray(places)) throw new Error("Dineway place search response did not include places/results.");
|
|
2227
|
+
return places.filter(isRecord);
|
|
2228
|
+
}
|
|
2229
|
+
function shadowContextFromGrant(cwd, platformApiUrl, grant, restaurant) {
|
|
2230
|
+
return {
|
|
2231
|
+
projectDir: cwd,
|
|
2232
|
+
platformApiUrl,
|
|
2233
|
+
authKind: "shadow-grant",
|
|
2234
|
+
accessToken: grant.accessToken,
|
|
2235
|
+
refreshToken: grant.refreshToken,
|
|
2236
|
+
user: grant.user,
|
|
2237
|
+
ossHost: platformApiUrl,
|
|
2238
|
+
placeId: grant.placeId,
|
|
2239
|
+
restaurantName: restaurant?.name ?? grant.restaurantName,
|
|
2240
|
+
city: restaurant?.city ?? grant.city
|
|
2241
|
+
};
|
|
2024
2242
|
}
|
|
2025
|
-
function
|
|
2026
|
-
return
|
|
2243
|
+
function grantFromShadowContext(context, grant, overrides = {}) {
|
|
2244
|
+
return {
|
|
2245
|
+
...grant,
|
|
2246
|
+
platformApiUrl: context.platformApiUrl,
|
|
2247
|
+
accessToken: context.accessToken ?? grant.accessToken,
|
|
2248
|
+
refreshToken: context.refreshToken ?? grant.refreshToken,
|
|
2249
|
+
placeId: context.placeId ?? grant.placeId,
|
|
2250
|
+
restaurantName: context.restaurantName ?? grant.restaurantName,
|
|
2251
|
+
city: context.city ?? grant.city,
|
|
2252
|
+
user: context.user ?? grant.user,
|
|
2253
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2254
|
+
...overrides
|
|
2255
|
+
};
|
|
2027
2256
|
}
|
|
2028
|
-
async function
|
|
2029
|
-
const
|
|
2257
|
+
async function searchPlacesWithShadowGrant(options) {
|
|
2258
|
+
const context = shadowContextFromGrant(options.cwd, options.platformApiUrl, options.grant, options.restaurant);
|
|
2259
|
+
const payload = await shadowUpgradeFetch(context, "/api/places/search", options.deps, {
|
|
2260
|
+
method: "POST",
|
|
2261
|
+
body: JSON.stringify({ textQuery: `${options.restaurant.name} in ${options.restaurant.city}` })
|
|
2262
|
+
});
|
|
2263
|
+
return {
|
|
2264
|
+
grant: grantFromShadowContext(context, options.grant),
|
|
2265
|
+
candidates: extractPlaceCandidates(payload)
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
async function bindShadowGrantPlace(options) {
|
|
2269
|
+
const context = shadowContextFromGrant(options.cwd, options.platformApiUrl, options.grant, options.restaurant);
|
|
2270
|
+
const payload = await shadowUpgradeFetch(context, "/api/auth/users/shadow/place", options.deps, {
|
|
2030
2271
|
method: "POST",
|
|
2031
|
-
headers: { "Content-Type": "application/json" },
|
|
2032
2272
|
body: JSON.stringify({
|
|
2033
|
-
placeId,
|
|
2273
|
+
placeId: options.placeId,
|
|
2034
2274
|
language: "en"
|
|
2035
2275
|
})
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2276
|
+
});
|
|
2277
|
+
const grant = grantFromShadowContext(context, options.grant, {
|
|
2278
|
+
placeId: options.placeId,
|
|
2279
|
+
accessToken: payload.accessToken ?? context.accessToken ?? options.grant.accessToken,
|
|
2280
|
+
refreshToken: payload.refreshToken ?? context.refreshToken ?? options.grant.refreshToken,
|
|
2281
|
+
restaurantName: options.restaurant.name,
|
|
2282
|
+
city: options.restaurant.city,
|
|
2283
|
+
user: payload.user ? normalizeForgewayUser(payload.user) : context.user ?? options.grant.user
|
|
2284
|
+
});
|
|
2285
|
+
await (options.deps.writeShadowGrant ?? writeForgewayShadowGrant)(grant, options.cwd);
|
|
2286
|
+
return grant;
|
|
2041
2287
|
}
|
|
2042
|
-
async function
|
|
2043
|
-
const
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2288
|
+
async function resolvePlaceIdWithShadowSearch(options) {
|
|
2289
|
+
const { grant, candidates } = await searchPlacesWithShadowGrant(options);
|
|
2290
|
+
if (candidates.length === 0) throw new Error(`Dineway place search returned no candidates for "${options.restaurant.name} in ${options.restaurant.city}".`);
|
|
2291
|
+
const selected = selectPlaceCandidate(candidates, {
|
|
2292
|
+
name: options.restaurant.name,
|
|
2293
|
+
city: options.restaurant.city
|
|
2294
|
+
});
|
|
2295
|
+
const finalPlaceId = selected ? candidatePlaceId(selected.candidate) : "";
|
|
2296
|
+
if (!PLACE_ID_PATTERN.test(finalPlaceId)) throw new Error("Could not resolve a valid Dineway place id for Forgeway deploy.");
|
|
2297
|
+
return await bindShadowGrantPlace({
|
|
2298
|
+
...options,
|
|
2299
|
+
grant,
|
|
2300
|
+
placeId: finalPlaceId
|
|
2301
|
+
});
|
|
2053
2302
|
}
|
|
2054
2303
|
async function createShadowGrant(options) {
|
|
2055
|
-
|
|
2056
|
-
const
|
|
2057
|
-
let finalPlaceId = options.placeId;
|
|
2058
|
-
if (!finalPlaceId) finalPlaceId = candidatePlaceId(selectPlaceCandidate(await searchPlaces(options.platformApiUrl, initialAuth.accessToken, options.restaurantName, options.city, options.deps), options.restaurantName, options.city));
|
|
2059
|
-
if (!PLACE_ID_PATTERN.test(finalPlaceId)) throw new Error("Could not resolve a valid Dineway place id for Forgeway deploy.");
|
|
2060
|
-
const finalAuth = finalPlaceId === initialPlaceId ? initialAuth : await createShadowUser(options.platformApiUrl, finalPlaceId, options.deps);
|
|
2304
|
+
if (options.placeId && !PLACE_ID_PATTERN.test(options.placeId)) throw new Error("Could not resolve a valid Dineway place id for Forgeway deploy.");
|
|
2305
|
+
const auth = await createShadowUser(options.platformApiUrl, options.placeId, options.deps);
|
|
2061
2306
|
const grant = {
|
|
2062
2307
|
platformApiUrl: options.platformApiUrl,
|
|
2063
|
-
placeId:
|
|
2064
|
-
accessToken:
|
|
2065
|
-
refreshToken:
|
|
2308
|
+
...options.placeId ? { placeId: options.placeId } : {},
|
|
2309
|
+
accessToken: auth.accessToken,
|
|
2310
|
+
refreshToken: auth.refreshToken,
|
|
2066
2311
|
restaurantName: options.restaurantName,
|
|
2067
2312
|
city: options.city,
|
|
2068
|
-
user:
|
|
2313
|
+
user: auth.user ? normalizeForgewayUser(auth.user) : void 0,
|
|
2069
2314
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2070
2315
|
};
|
|
2071
2316
|
await (options.deps.writeShadowGrant ?? writeForgewayShadowGrant)(grant, options.cwd);
|
|
@@ -2158,6 +2403,25 @@ async function getDeploymentSite(context, site, deps) {
|
|
|
2158
2403
|
throw error;
|
|
2159
2404
|
}
|
|
2160
2405
|
}
|
|
2406
|
+
async function getDeploymentSiteByPlaceId(context, placeId, deps) {
|
|
2407
|
+
try {
|
|
2408
|
+
return normalizeSiteDetail(await ossFetch(context, `/api/deployments/sites/by-place/${encodeURIComponent(placeId)}`, deps));
|
|
2409
|
+
} catch (error) {
|
|
2410
|
+
if (error instanceof ForgewayApiError && error.status === 404) return null;
|
|
2411
|
+
throw error;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
async function ensureRestaurantClaim(context, restaurant, deps) {
|
|
2415
|
+
if (!context.placeId) throw new Error("Forgeway restaurant deploy is missing a place ID for claim ownership.");
|
|
2416
|
+
await ossFetch(context, "/api/restaurant-claims/ensure", deps, {
|
|
2417
|
+
method: "POST",
|
|
2418
|
+
body: JSON.stringify({
|
|
2419
|
+
placeId: context.placeId,
|
|
2420
|
+
...restaurant?.name ? { placeName: restaurant.name } : {},
|
|
2421
|
+
...restaurant?.city ? { city: restaurant.city } : {}
|
|
2422
|
+
})
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2161
2425
|
async function createDeploymentSite(context, input, deps) {
|
|
2162
2426
|
return await ossFetch(context, "/api/deployments/sites", deps, {
|
|
2163
2427
|
method: "POST",
|
|
@@ -2175,10 +2439,12 @@ async function resolveDeploymentSite(cwd, context, options, seedPath, deps, preR
|
|
|
2175
2439
|
const saved = pkg ? getSavedForgewayMetadata(pkg) : {};
|
|
2176
2440
|
const explicitSite = options.site;
|
|
2177
2441
|
const savedSite = typeof saved.siteId === "string" ? saved.siteId : saved.siteSlug;
|
|
2178
|
-
const siteRef = explicitSite ?? savedSite;
|
|
2179
|
-
const
|
|
2442
|
+
const siteRef = context.placeId ? explicitSite : explicitSite ?? savedSite;
|
|
2443
|
+
const existingByPlace = context.placeId ? await getDeploymentSiteByPlaceId(context, context.placeId, deps) : null;
|
|
2444
|
+
const restaurant = preResolvedRestaurant?.name && preResolvedRestaurant.city ? preResolvedRestaurant : await resolveRestaurantInfo(options, seedPath, deps, !existingByPlace && !siteRef);
|
|
2180
2445
|
const targetSlug = siteRef ?? deriveForgewaySiteSlug(restaurant.name || "", restaurant.city || "");
|
|
2181
|
-
let site =
|
|
2446
|
+
let site = existingByPlace;
|
|
2447
|
+
if (!site && siteRef) site = await getDeploymentSite(context, targetSlug, deps);
|
|
2182
2448
|
if (!site) site = await createDeploymentSite(context, {
|
|
2183
2449
|
slug: targetSlug,
|
|
2184
2450
|
restaurant,
|
|
@@ -2200,20 +2466,52 @@ function redactDatabaseOutput(value, secrets) {
|
|
|
2200
2466
|
for (const secret of secrets) if (secret.length > 0) redacted = redacted.split(secret).join("[redacted]");
|
|
2201
2467
|
return redacted.replace(JWT_PATTERN, "[redacted-token]").replace(TOKEN_ASSIGNMENT_PATTERN, "$1=[redacted]");
|
|
2202
2468
|
}
|
|
2469
|
+
async function hashFileSha256(filePath) {
|
|
2470
|
+
const hash = createHash("sha256");
|
|
2471
|
+
for await (const chunk of createReadStream(filePath)) hash.update(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2472
|
+
return `sha256:${hash.digest("hex")}`;
|
|
2473
|
+
}
|
|
2474
|
+
async function buildDinewayInitializationFingerprint(seedPath) {
|
|
2475
|
+
const migration = getMigrationFingerprint();
|
|
2476
|
+
return {
|
|
2477
|
+
version: 1,
|
|
2478
|
+
migrationHash: migration.migrationHash,
|
|
2479
|
+
migrationCount: migration.migrationCount,
|
|
2480
|
+
seedHash: seedPath ? await hashFileSha256(seedPath) : null
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
function initializationFingerprintsMatch(left, right) {
|
|
2484
|
+
return left?.version === right.version && left.migrationHash === right.migrationHash && left.migrationCount === right.migrationCount && left.seedHash === right.seedHash;
|
|
2485
|
+
}
|
|
2486
|
+
function localDatabaseInitializationMatches(metadata, siteId, databaseId, fingerprint) {
|
|
2487
|
+
return metadata?.status === "succeeded" && metadata.siteId === siteId && metadata.databaseId === databaseId && initializationFingerprintsMatch(metadata.fingerprint, fingerprint);
|
|
2488
|
+
}
|
|
2489
|
+
async function readLocalDatabaseInitializationMetadata(cwd) {
|
|
2490
|
+
const pkg = await readDeployPackageJson(cwd).catch(() => null);
|
|
2491
|
+
if (!pkg) return void 0;
|
|
2492
|
+
return getSavedForgewayMetadata(pkg).databaseInitialization;
|
|
2493
|
+
}
|
|
2494
|
+
async function writeLocalDatabaseInitializationMetadata(cwd, metadata) {
|
|
2495
|
+
await writeForgewayDeployMetadata(cwd, { databaseInitialization: {
|
|
2496
|
+
...metadata,
|
|
2497
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2498
|
+
} });
|
|
2499
|
+
}
|
|
2203
2500
|
async function runDinewayInitialization(options) {
|
|
2204
2501
|
const db = createDatabase({
|
|
2205
2502
|
url: options.credentials.databaseUrl,
|
|
2206
2503
|
authToken: options.credentials.authToken
|
|
2207
2504
|
});
|
|
2208
2505
|
try {
|
|
2209
|
-
await runMigrations(db);
|
|
2506
|
+
const migrations = await runMigrations(db);
|
|
2507
|
+
let seedResult = null;
|
|
2210
2508
|
if (options.seedPath) {
|
|
2211
2509
|
const seed = JSON.parse(await readFile(options.seedPath, "utf-8"));
|
|
2212
2510
|
const validation = validateSeed(seed);
|
|
2213
2511
|
if (!validation.valid) throw new Error(`Seed validation failed: ${validation.errors.join("; ")}`);
|
|
2214
2512
|
const uploadsDir = resolve(options.cwd, "uploads");
|
|
2215
2513
|
await mkdir(uploadsDir, { recursive: true });
|
|
2216
|
-
await applySeed(db, seed, {
|
|
2514
|
+
seedResult = await applySeed(db, seed, {
|
|
2217
2515
|
includeContent: true,
|
|
2218
2516
|
onConflict: "skip",
|
|
2219
2517
|
storage: new LocalStorage({
|
|
@@ -2222,6 +2520,10 @@ async function runDinewayInitialization(options) {
|
|
|
2222
2520
|
})
|
|
2223
2521
|
});
|
|
2224
2522
|
}
|
|
2523
|
+
return {
|
|
2524
|
+
migrations,
|
|
2525
|
+
seed: seedResult
|
|
2526
|
+
};
|
|
2225
2527
|
} catch (error) {
|
|
2226
2528
|
const message = error instanceof Error ? error.message : String(error);
|
|
2227
2529
|
throw new Error(redactDatabaseOutput(message, [options.credentials.authToken, options.credentials.databaseUrl]), { cause: error });
|
|
@@ -2252,25 +2554,75 @@ async function recordSiteDatabaseInitialization(context, siteId, databaseId, sta
|
|
|
2252
2554
|
});
|
|
2253
2555
|
}
|
|
2254
2556
|
async function initializeSiteDatabaseBeforeDeploy(options) {
|
|
2557
|
+
const fingerprint = await buildDinewayInitializationFingerprint(options.seedPath);
|
|
2558
|
+
const bindStart = Date.now();
|
|
2255
2559
|
const databaseId = (await bindSiteDatabase(options.context, options.siteId, options.database, options.deps)).binding.database.id;
|
|
2560
|
+
if (options.verbose) consola.info(`Database bound in ${formatDuration(Date.now() - bindStart)}`);
|
|
2561
|
+
const localInitialization = await readLocalDatabaseInitializationMetadata(options.cwd);
|
|
2562
|
+
if (!options.force && localDatabaseInitializationMatches(localInitialization, options.siteId, databaseId, fingerprint)) return {
|
|
2563
|
+
status: "skipped",
|
|
2564
|
+
databaseId,
|
|
2565
|
+
fingerprint
|
|
2566
|
+
};
|
|
2567
|
+
const revealStart = Date.now();
|
|
2256
2568
|
const credentials = await revealDatabaseRuntimeCredentials(options.context, databaseId, options.deps);
|
|
2569
|
+
if (options.verbose) consola.info(`Database credentials revealed in ${formatDuration(Date.now() - revealStart)}`);
|
|
2257
2570
|
try {
|
|
2258
|
-
await (options.deps.runDinewayInitialization ?? runDinewayInitialization)({
|
|
2571
|
+
const result = await (options.deps.runDinewayInitialization ?? runDinewayInitialization)({
|
|
2259
2572
|
cwd: options.cwd,
|
|
2260
2573
|
credentials,
|
|
2261
2574
|
seedPath: options.seedPath
|
|
2262
2575
|
});
|
|
2263
2576
|
await recordSiteDatabaseInitialization(options.context, options.siteId, credentials.databaseId || databaseId, "succeeded", void 0, options.deps);
|
|
2577
|
+
await writeLocalDatabaseInitializationMetadata(options.cwd, {
|
|
2578
|
+
siteId: options.siteId,
|
|
2579
|
+
databaseId: credentials.databaseId || databaseId,
|
|
2580
|
+
status: "succeeded",
|
|
2581
|
+
fingerprint
|
|
2582
|
+
});
|
|
2583
|
+
return {
|
|
2584
|
+
status: "initialized",
|
|
2585
|
+
databaseId: credentials.databaseId || databaseId,
|
|
2586
|
+
fingerprint,
|
|
2587
|
+
result
|
|
2588
|
+
};
|
|
2264
2589
|
} catch (error) {
|
|
2265
2590
|
const message = redactDatabaseOutput(error instanceof Error ? error.message : String(error), [credentials.authToken, credentials.databaseUrl]);
|
|
2266
2591
|
await recordSiteDatabaseInitialization(options.context, options.siteId, credentials.databaseId || databaseId, "failed", message, options.deps).catch(() => void 0);
|
|
2592
|
+
await writeLocalDatabaseInitializationMetadata(options.cwd, {
|
|
2593
|
+
siteId: options.siteId,
|
|
2594
|
+
databaseId: credentials.databaseId || databaseId,
|
|
2595
|
+
status: "failed",
|
|
2596
|
+
fingerprint,
|
|
2597
|
+
error: message.slice(0, 2e3)
|
|
2598
|
+
}).catch(() => void 0);
|
|
2267
2599
|
throw new Error(message, { cause: error });
|
|
2268
2600
|
}
|
|
2269
2601
|
}
|
|
2270
|
-
function
|
|
2602
|
+
function normalizeIgnorePattern(pattern) {
|
|
2603
|
+
return pattern.trim().replace(BACKSLASH_PATTERN, "/").replace(LEADING_SLASHES_PATTERN, "").replace(TRAILING_SLASHES_PATTERN, "");
|
|
2604
|
+
}
|
|
2605
|
+
function wildcardPatternToRegExp(pattern) {
|
|
2606
|
+
const source = pattern.split("**").map((part) => part.split("*").map((segment) => segment.replace(REGEXP_ESCAPE_PATTERN, "\\$&")).join("[^/]*")).join(".*");
|
|
2607
|
+
return new RegExp(`^${source}$`);
|
|
2608
|
+
}
|
|
2609
|
+
function matchesIgnorePattern(path, rawPattern) {
|
|
2610
|
+
const pattern = normalizeIgnorePattern(rawPattern);
|
|
2611
|
+
if (!pattern || pattern.startsWith("#")) return false;
|
|
2612
|
+
if (pattern.includes("*")) return wildcardPatternToRegExp(pattern).test(path);
|
|
2613
|
+
if (!pattern.includes("/")) return path === pattern || path.startsWith(`${pattern}/`) || basename(path) === pattern;
|
|
2614
|
+
return path === pattern || path.startsWith(`${pattern}/`);
|
|
2615
|
+
}
|
|
2616
|
+
async function readDeploymentIgnorePatterns(sourceDir) {
|
|
2617
|
+
const ignorePath = join(sourceDir, ".dinewayignore");
|
|
2618
|
+
if (!await fileExists$8(ignorePath)) return [];
|
|
2619
|
+
return (await readFile(ignorePath, "utf-8")).split(LINE_SPLIT_PATTERN).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
2620
|
+
}
|
|
2621
|
+
function shouldExclude(name, customPatterns = []) {
|
|
2271
2622
|
const normalized = name.replace(BACKSLASH_PATTERN, "/");
|
|
2272
2623
|
for (const pattern of EXCLUDE_PATTERNS) if (normalized === pattern || normalized.startsWith(`${pattern}/`) || normalized.endsWith(`/${pattern}`) || normalized.includes(`/${pattern}/`)) return true;
|
|
2273
|
-
|
|
2624
|
+
if (normalized.endsWith(".log")) return true;
|
|
2625
|
+
return customPatterns.some((pattern) => matchesIgnorePattern(normalized, pattern));
|
|
2274
2626
|
}
|
|
2275
2627
|
function normalizeRelativePath(sourceDir, absolutePath) {
|
|
2276
2628
|
return relative(sourceDir, absolutePath).replace(BACKSLASH_PATTERN, "/");
|
|
@@ -2289,6 +2641,8 @@ async function hashFile(filePath) {
|
|
|
2289
2641
|
};
|
|
2290
2642
|
}
|
|
2291
2643
|
async function collectDeploymentFiles(sourceDir) {
|
|
2644
|
+
const customIgnorePatterns = await readDeploymentIgnorePatterns(sourceDir);
|
|
2645
|
+
const candidates = [];
|
|
2292
2646
|
const files = [];
|
|
2293
2647
|
async function walk(currentDir) {
|
|
2294
2648
|
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
@@ -2296,22 +2650,29 @@ async function collectDeploymentFiles(sourceDir) {
|
|
|
2296
2650
|
for (const entry of entries) {
|
|
2297
2651
|
const absolutePath = join(currentDir, entry.name);
|
|
2298
2652
|
const normalizedPath = normalizeRelativePath(sourceDir, absolutePath);
|
|
2299
|
-
if (!normalizedPath || shouldExclude(normalizedPath)) continue;
|
|
2653
|
+
if (!normalizedPath || shouldExclude(normalizedPath, customIgnorePatterns)) continue;
|
|
2300
2654
|
if (entry.isDirectory()) {
|
|
2301
2655
|
await walk(absolutePath);
|
|
2302
2656
|
continue;
|
|
2303
2657
|
}
|
|
2304
2658
|
if (!entry.isFile()) continue;
|
|
2305
|
-
|
|
2306
|
-
files.push({
|
|
2659
|
+
candidates.push({
|
|
2307
2660
|
absolutePath,
|
|
2308
|
-
path: normalizedPath
|
|
2309
|
-
sha,
|
|
2310
|
-
size
|
|
2661
|
+
path: normalizedPath
|
|
2311
2662
|
});
|
|
2312
2663
|
}
|
|
2313
2664
|
}
|
|
2314
2665
|
await walk(sourceDir);
|
|
2666
|
+
await runWithConcurrency(candidates, FILE_HASH_CONCURRENCY, async ({ absolutePath, path }) => {
|
|
2667
|
+
const { sha, size } = await hashFile(absolutePath);
|
|
2668
|
+
files.push({
|
|
2669
|
+
absolutePath,
|
|
2670
|
+
path,
|
|
2671
|
+
sha,
|
|
2672
|
+
size
|
|
2673
|
+
});
|
|
2674
|
+
});
|
|
2675
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
2315
2676
|
return files;
|
|
2316
2677
|
}
|
|
2317
2678
|
async function runWithConcurrency(items, concurrency, worker) {
|
|
@@ -2349,16 +2710,23 @@ function getDeploymentError(metadata) {
|
|
|
2349
2710
|
if (!metadata || typeof metadata.error !== "object" || metadata.error === null) return null;
|
|
2350
2711
|
return metadata.error.errorMessage ?? null;
|
|
2351
2712
|
}
|
|
2352
|
-
async function pollDeployment(context, deploymentId, deps) {
|
|
2713
|
+
async function pollDeployment(context, deploymentId, deps, verbose) {
|
|
2353
2714
|
const pollIntervalMs = deps.pollIntervalMs ?? POLL_INTERVAL_MS$1;
|
|
2354
2715
|
const pollTimeoutMs = deps.pollTimeoutMs ?? POLL_TIMEOUT_MS$1;
|
|
2355
2716
|
const startTime = Date.now();
|
|
2356
2717
|
let deployment = null;
|
|
2718
|
+
let firstPoll = true;
|
|
2719
|
+
let lastStatus = null;
|
|
2357
2720
|
while (Date.now() - startTime < pollTimeoutMs) {
|
|
2358
|
-
if (pollIntervalMs > 0) await new Promise((done) => setTimeout(done, pollIntervalMs));
|
|
2721
|
+
if (!firstPoll && pollIntervalMs > 0) await new Promise((done) => setTimeout(done, pollIntervalMs));
|
|
2722
|
+
firstPoll = false;
|
|
2359
2723
|
await ossFetch(context, `/api/deployments/${encodeURIComponent(deploymentId)}/sync`, deps, { method: "POST" }).catch(() => void 0);
|
|
2360
2724
|
deployment = await ossFetch(context, `/api/deployments/${encodeURIComponent(deploymentId)}`, deps);
|
|
2361
2725
|
const status = deployment.status.toUpperCase();
|
|
2726
|
+
if (verbose || status !== lastStatus) {
|
|
2727
|
+
consola.info(`Deployment status: ${status} (${formatDuration(Date.now() - startTime)} elapsed)`);
|
|
2728
|
+
lastStatus = status;
|
|
2729
|
+
}
|
|
2362
2730
|
if (status === "READY") break;
|
|
2363
2731
|
if (status === "ERROR" || status === "CANCELED") throw new Error(getDeploymentError(deployment.metadata) ?? `Deployment failed with status: ${deployment.status}`);
|
|
2364
2732
|
if (pollIntervalMs === 0) break;
|
|
@@ -2372,8 +2740,13 @@ async function pollDeployment(context, deploymentId, deps) {
|
|
|
2372
2740
|
}
|
|
2373
2741
|
async function deploySiteProject(options) {
|
|
2374
2742
|
options.deps.onEvent?.("scan");
|
|
2743
|
+
const scanStart = Date.now();
|
|
2744
|
+
consola.start("Scanning deploy files");
|
|
2375
2745
|
const localFiles = await (options.deps.collectDeploymentFiles ?? collectDeploymentFiles)(options.sourceDir);
|
|
2376
2746
|
if (localFiles.length === 0) throw new Error("No deployable files found in the source directory.");
|
|
2747
|
+
consola.success(`Scanned ${localFiles.length} files (${formatBytes$1(deploymentFileSize(localFiles))}) in ${formatDuration(Date.now() - scanStart)}`);
|
|
2748
|
+
const createStart = Date.now();
|
|
2749
|
+
consola.start("Creating Forgeway deployment");
|
|
2377
2750
|
const createResult = await ossFetch(options.context, `/api/deployments/sites/${encodeURIComponent(options.siteId)}/deploy`, options.deps, {
|
|
2378
2751
|
method: "POST",
|
|
2379
2752
|
body: JSON.stringify({
|
|
@@ -2385,15 +2758,25 @@ async function deploySiteProject(options) {
|
|
|
2385
2758
|
}))
|
|
2386
2759
|
})
|
|
2387
2760
|
});
|
|
2761
|
+
if (options.verbose) consola.info(`Deployment manifest accepted in ${formatDuration(Date.now() - createStart)}`);
|
|
2388
2762
|
const localFileByPath = new Map(localFiles.map((file) => [file.path, file]));
|
|
2389
|
-
|
|
2763
|
+
const pendingFiles = createResult.files.filter((file) => !file.uploadedAt);
|
|
2764
|
+
const pendingSize = pendingFiles.reduce((total, file) => total + file.size, 0);
|
|
2765
|
+
if (pendingFiles.length === 0) consola.success("No file uploads needed");
|
|
2766
|
+
else consola.start(`Uploading ${pendingFiles.length} changed files (${formatBytes$1(pendingSize)})`);
|
|
2767
|
+
let uploadedCount = 0;
|
|
2768
|
+
await runWithConcurrency(pendingFiles, DIRECT_UPLOAD_CONCURRENCY, async (manifestFile) => {
|
|
2390
2769
|
const localFile = localFileByPath.get(manifestFile.path);
|
|
2391
2770
|
if (!localFile) throw new Error(`Forgeway returned an unknown file path: ${manifestFile.path}`);
|
|
2392
2771
|
if (localFile.sha !== manifestFile.sha || localFile.size !== manifestFile.size) throw new Error(`Forgeway file metadata mismatch for: ${manifestFile.path}`);
|
|
2393
2772
|
await uploadDirectDeploymentFile(options.context, createResult.id, manifestFile, localFile, options.deps);
|
|
2773
|
+
uploadedCount += 1;
|
|
2774
|
+
if (options.verbose || uploadedCount === pendingFiles.length || uploadedCount % 10 === 0) consola.info(`Uploaded ${uploadedCount}/${pendingFiles.length} files`);
|
|
2394
2775
|
});
|
|
2776
|
+
consola.start("Starting Forgeway deployment");
|
|
2395
2777
|
await startDirectDeployment(options.context, createResult.id, options.startBody, options.deps);
|
|
2396
|
-
|
|
2778
|
+
consola.start("Waiting for Forgeway deployment");
|
|
2779
|
+
const result = await pollDeployment(options.context, createResult.id, options.deps, options.verbose);
|
|
2397
2780
|
return {
|
|
2398
2781
|
deploymentId: createResult.id,
|
|
2399
2782
|
...result,
|
|
@@ -2423,23 +2806,33 @@ async function deployForgeway(cwd, options, deps = {}) {
|
|
|
2423
2806
|
const sourceDir = resolve(cwd);
|
|
2424
2807
|
if (!(await stat(sourceDir).catch(() => null))?.isDirectory()) throw new Error(`"${sourceDir}" is not a valid directory.`);
|
|
2425
2808
|
if (EXCLUDE_PATTERNS.includes(basename(sourceDir))) throw new Error(`"${basename(sourceDir)}" is an excluded directory and cannot be deployed.`);
|
|
2426
|
-
const
|
|
2809
|
+
const verbose = isVerboseDeploy(options);
|
|
2810
|
+
const seedPath = options.skipSeed ? null : await resolveSeedPath$1(cwd, options.seed);
|
|
2811
|
+
if (options.skipSeed) consola.info("Skipping seed initialization by request");
|
|
2427
2812
|
const { context, restaurant } = await resolveProjectContext(cwd, options, deps);
|
|
2813
|
+
await ensureRestaurantClaim(context, restaurant, deps);
|
|
2428
2814
|
const { site } = await resolveDeploymentSite(cwd, context, options, seedPath, deps, restaurant);
|
|
2429
2815
|
const database = parseDatabaseMode(options.database);
|
|
2430
2816
|
if (database !== "none") {
|
|
2431
2817
|
consola.start("Initializing Forgeway managed database");
|
|
2432
2818
|
try {
|
|
2433
2819
|
deps.onEvent?.("initialize");
|
|
2434
|
-
await initializeSiteDatabaseBeforeDeploy({
|
|
2820
|
+
const initialization = await initializeSiteDatabaseBeforeDeploy({
|
|
2435
2821
|
context,
|
|
2436
2822
|
siteId: site.id,
|
|
2437
2823
|
database,
|
|
2438
2824
|
cwd,
|
|
2439
2825
|
seedPath,
|
|
2826
|
+
force: options.forceDbInit === true,
|
|
2827
|
+
verbose,
|
|
2440
2828
|
deps
|
|
2441
2829
|
});
|
|
2442
|
-
consola.success("Database initialized");
|
|
2830
|
+
if (initialization.status === "skipped") consola.success("Database already initialized for current schema and seed (local metadata)");
|
|
2831
|
+
else {
|
|
2832
|
+
const applied = initialization.result?.migrations.applied.length ?? 0;
|
|
2833
|
+
const seedSummary = formatSeedSummary(initialization.result?.seed);
|
|
2834
|
+
consola.success(`Database initialized: ${applied} migrations applied, ${seedSummary}`);
|
|
2835
|
+
}
|
|
2443
2836
|
} catch (error) {
|
|
2444
2837
|
consola.error("Database initialization failed");
|
|
2445
2838
|
throw error;
|
|
@@ -2451,7 +2844,8 @@ async function deployForgeway(cwd, options, deps = {}) {
|
|
|
2451
2844
|
siteId: site.id,
|
|
2452
2845
|
sourceDir,
|
|
2453
2846
|
startBody: {},
|
|
2454
|
-
deps
|
|
2847
|
+
deps,
|
|
2848
|
+
verbose
|
|
2455
2849
|
});
|
|
2456
2850
|
const url = deployment.liveUrl ?? `https://${site.domain}`;
|
|
2457
2851
|
await (deps.writeProjectEnv ?? persistForgewayCliEnv)(cwd, {
|
|
@@ -2994,6 +3388,11 @@ const deployCommand = defineCommand({
|
|
|
2994
3388
|
description: "Forgeway deployment site ID or slug",
|
|
2995
3389
|
required: false
|
|
2996
3390
|
},
|
|
3391
|
+
"place-id": {
|
|
3392
|
+
type: "string",
|
|
3393
|
+
description: "Restaurant Google place ID for Forgeway claim and site recovery",
|
|
3394
|
+
required: false
|
|
3395
|
+
},
|
|
2997
3396
|
email: {
|
|
2998
3397
|
type: "string",
|
|
2999
3398
|
description: "Dineway account email for deploy authorization",
|
|
@@ -3018,6 +3417,21 @@ const deployCommand = defineCommand({
|
|
|
3018
3417
|
type: "string",
|
|
3019
3418
|
description: "Seed file path for Forgeway Dineway initialization",
|
|
3020
3419
|
required: false
|
|
3420
|
+
},
|
|
3421
|
+
"skip-seed": {
|
|
3422
|
+
type: "boolean",
|
|
3423
|
+
description: "Skip applying seed data during Forgeway database initialization",
|
|
3424
|
+
default: false
|
|
3425
|
+
},
|
|
3426
|
+
"force-db-init": {
|
|
3427
|
+
type: "boolean",
|
|
3428
|
+
description: "Force Forgeway database migrations and seed even when initialization fingerprints match",
|
|
3429
|
+
default: false
|
|
3430
|
+
},
|
|
3431
|
+
verbose: {
|
|
3432
|
+
type: "boolean",
|
|
3433
|
+
description: "Show detailed Forgeway deploy progress and timing logs",
|
|
3434
|
+
default: false
|
|
3021
3435
|
}
|
|
3022
3436
|
},
|
|
3023
3437
|
async run({ args }) {
|
|
@@ -3029,11 +3443,15 @@ const deployCommand = defineCommand({
|
|
|
3029
3443
|
yes: args.yes,
|
|
3030
3444
|
dryRun: args["dry-run"],
|
|
3031
3445
|
site: args.site,
|
|
3446
|
+
placeId: args["place-id"],
|
|
3032
3447
|
email: args.email,
|
|
3033
3448
|
restaurantName: args["restaurant-name"],
|
|
3034
3449
|
city: args.city,
|
|
3035
3450
|
database: args.database,
|
|
3036
|
-
seed: args.seed
|
|
3451
|
+
seed: args.seed,
|
|
3452
|
+
skipSeed: args["skip-seed"],
|
|
3453
|
+
forceDbInit: args["force-db-init"],
|
|
3454
|
+
verbose: args.verbose
|
|
3037
3455
|
};
|
|
3038
3456
|
consola.start(`Preparing ${target.label} deploy`);
|
|
3039
3457
|
await ensureTargetCli(target, cwd, {
|