@underpostnet/cyberia 3.3.73 → 3.3.77
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/.env.example +17 -5
- package/.github/workflows/coverall.cyberia.ci.yml +10 -0
- package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
- package/.github/workflows/engine-cyberia.ci.yml +1 -1
- package/.github/workflows/publish.cyberia.ci.yml +44 -0
- package/AGENTS.md +61 -12
- package/CHANGELOG.md +159 -161
- package/CLI-HELP.md +20 -13
- package/Dockerfile +1 -1
- package/Dockerfile.dev +1 -1
- package/Dockerfile.test +2 -2
- package/README.md +28 -28
- package/bin/build.js +96 -21
- package/bin/cyberia.js +1247 -1373
- package/bin/index.js +1247 -1373
- package/compose.env +17 -5
- package/conf.js +7 -4
- package/deploy/cyberia-client/deploy.sh +10 -11
- package/deploy/cyberia-client/package.sh +0 -1
- package/deploy/cyberia-client/state.sh +3 -2
- package/deploy/cyberia-server/deploy.sh +10 -11
- package/deploy/cyberia-server/package.sh +0 -1
- package/deploy/cyberia-server/state.sh +3 -2
- package/deploy/dd-cyberia/deploy.sh +5 -5
- package/deploy/dd-cyberia/init.sh +15 -14
- package/deploy/dd-cyberia/package.sh +0 -1
- package/deploy/dd-cyberia/state.sh +2 -2
- package/deploy/dd-cyberia/sync-deploy.sh +139 -75
- package/deploy/lib/config.sh +29 -0
- package/deploy/lib/github-actions-logging.sh +92 -76
- package/deploy/lib/host.sh +28 -19
- package/deploy/lib/state.sh +4 -1
- package/deploy/pwa-microservices-template/deploy.sh +10 -8
- package/deploy/release/deploy.sh +0 -2
- package/deployment.yaml +1 -1
- package/docker-compose.yml +23 -17
- package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
- package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
- package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
- package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
- package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
- package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
- package/docs/coverage/cyberia/base.css +362 -0
- package/docs/coverage/cyberia/block-navigation.js +82 -0
- package/docs/coverage/cyberia/favicon.png +0 -0
- package/docs/coverage/cyberia/index.html +161 -0
- package/docs/coverage/cyberia/prettify.css +101 -0
- package/docs/coverage/cyberia/prettify.js +937 -0
- package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
- package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
- package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
- package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
- package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
- package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
- package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
- package/docs/coverage/cyberia/sorter.js +205 -0
- package/hardhat/package-lock.json +2 -2
- package/hardhat/package.json +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/mongo-express/deployment.yaml +46 -27
- package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
- package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
- package/manifests/deployment/mongo-express/service.yaml +16 -0
- package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
- package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
- package/package.json +19 -17
- package/scripts/test-monitor.sh +3 -3
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
- package/src/api/cyberia-action/cyberia-action.router.js +2 -2
- package/src/api/cyberia-action/cyberia-action.service.js +3 -4
- package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
- package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
- package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
- package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
- package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
- package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
- package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
- package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
- package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
- package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
- package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
- package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
- package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
- package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
- package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
- package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
- package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
- package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
- package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
- package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
- package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
- package/src/api/cyberia-map/cyberia-map.service.js +6 -2
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
- package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
- package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
- package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
- package/src/api/file/file.ref.js +153 -0
- package/src/api/file/file.ref.json +11 -2
- package/src/api/object-layer/object-layer.model.js +47 -12
- package/src/api/object-layer/object-layer.router.js +10 -3
- package/src/api/object-layer/object-layer.service.js +113 -120
- package/src/cli/app.js +12 -1
- package/src/cli/client.js +2 -2
- package/src/cli/cluster.js +80 -18
- package/src/cli/deploy.js +215 -34
- package/src/cli/docker-compose.js +1 -1
- package/src/cli/fs-selection.js +115 -0
- package/src/cli/fs.js +158 -342
- package/src/cli/index.js +22 -10
- package/src/cli/release.js +3 -1
- package/src/cli/repository.js +35 -9
- package/src/cli/run.js +80 -199
- package/src/cli/test.js +4 -0
- package/src/cli/wireguard.js +32 -31
- package/src/client/components/core/Badge.js +0 -1
- package/src/client/components/core/CssCore.js +8 -1
- package/src/client/components/core/Docs.js +77 -100
- package/src/client/components/core/Modal.js +77 -23
- package/src/client/components/core/ToolTip.js +15 -0
- package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
- package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
- package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
- package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
- package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
- package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
- package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
- package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
- package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
- package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
- package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
- package/src/client/components/default/AppShellDefault.js +1 -4
- package/src/client/components/itemledger/AppShellItemledger.js +0 -2
- package/src/client/components/underpost/AppShellUnderpost.js +1 -1
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
- package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
- package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
- package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
- package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
- package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
- package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
- package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
- package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
- package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
- package/src/client/services/object-layer/object-layer.management.js +6 -10
- package/src/client/services/object-layer/object-layer.service.js +3 -1
- package/src/client/ssr/views/Cyberia404.js +3 -3
- package/src/client-builder/client-build-docs.js +52 -52
- package/src/client-builder/client-build.js +67 -44
- package/src/client-builder/client-bundle.js +347 -0
- package/src/db/mongo/MongoExpress.js +187 -0
- package/src/grpc/cyberia/grpc-server.js +5 -7
- package/src/index.js +1 -1
- package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
- package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
- package/src/projects/cyberia/besu-genesis-generator.js +5 -14
- package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
- package/src/projects/cyberia/catalog-cyberia.js +3 -2
- package/src/projects/cyberia/gemini-client.js +3 -3
- package/src/projects/cyberia/generate-saga.js +4 -11
- package/src/projects/cyberia/hot-reload-trigger.js +17 -28
- package/src/projects/cyberia/instance-backup.js +210 -0
- package/src/projects/cyberia/instance-data.js +177 -257
- package/src/projects/cyberia/map-preview-generator.js +32 -68
- package/src/projects/cyberia/object-layer.js +152 -319
- package/src/projects/cyberia/seed-audio.js +139 -0
- package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
- package/src/projects/cyberia/semantic-layer-generator.js +2 -18
- package/src/projects/cyberia/stat-balance.js +81 -0
- package/src/projects/cyberia/stat-commands.js +23 -0
- package/src/projects/cyberia/stat-contract-generator.js +101 -0
- package/src/runtime/cyberia-client/Dockerfile +8 -4
- package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
- package/src/runtime/cyberia-server/Dockerfile +9 -2
- package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
- package/src/runtime/engine-cyberia/Dockerfile +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
- package/src/runtime/engine-cyberia/compose.env +17 -5
- package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
- package/src/server/build/coverage.js +113 -42
- package/src/server/build/package.js +55 -12
- package/src/server/build/testing.js +59 -9
- package/src/server/network/middlewares.js +8 -2
- package/src/server/network/underpost-gateway.js +113 -14
- package/src/server/ops/logger.js +70 -35
- package/src/server/runtime/conf.js +40 -9
- package/src/server/runtime/start.js +11 -2
- package/src/server/storage/data-query.js +16 -0
- package/src/server/storage/downloader.js +62 -14
- package/src/server/storage/zip.js +153 -0
- package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
- package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
- package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
- package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
- package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
- package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
- package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
- package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
- package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
- package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
- package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
- package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
- package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
- package/test/integration/app/cyberia/seed-audio.test.js +275 -0
- package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
- package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
- package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
- package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
- package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
- package/test/unit/catalog.test.js +13 -2
- package/test/unit/client-build-docs.test.js +97 -70
- package/test/unit/client-bundle.test.js +313 -0
- package/test/unit/conf-resolution.test.js +58 -7
- package/test/unit/coverage-artifact.test.js +120 -29
- package/test/unit/cyberia/instance-backup.test.js +183 -0
- package/test/unit/cyberia/instance-data.test.js +314 -0
- package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
- package/test/unit/cyberia/publish-workflow.test.js +22 -0
- package/test/unit/cyberia/stat-balance.test.js +69 -0
- package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
- package/test/unit/deploy-log-table.test.js +76 -5
- package/test/unit/downloader-integrity.test.js +98 -0
- package/test/unit/file-reference-registry.test.js +141 -0
- package/test/unit/fs-storage-paths.test.js +596 -0
- package/test/unit/gateway-pod-fetch.test.js +95 -0
- package/test/unit/letsencrypt-issuer.test.js +101 -0
- package/test/unit/logger-redaction.test.js +29 -0
- package/test/unit/package.test.js +70 -16
- package/test/unit/prepare-host.test.js +84 -1
- package/test/unit/release-bump.test.js +3 -1
- package/test/unit/start-options.test.js +12 -0
- package/test/unit/test-tiers.test.js +27 -0
- package/test/unit/zip-archive.test.js +142 -0
- package/vitest.config.js +11 -3
- package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
- package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
|
@@ -2,9 +2,8 @@ import { Schema, model } from 'mongoose';
|
|
|
2
2
|
|
|
3
3
|
// https://mongoosejs.com/docs/2.7.x/docs/schematypes.html
|
|
4
4
|
|
|
5
|
-
// Expanded metadata for one logic event a trigger item can fire.
|
|
6
|
-
// `skills[]` entries
|
|
7
|
-
// SkillDefinition consumed by cyberia-server (game/skill.go).
|
|
5
|
+
// Expanded metadata for one logic event a trigger item can fire. Same shape as
|
|
6
|
+
// the `skills[]` entries of DefaultSkillConfig (cyberia-server-defaults.js).
|
|
8
7
|
const SkillDefinitionSchema = new Schema(
|
|
9
8
|
{
|
|
10
9
|
// Handler key dispatched by the simulation skill engine (e.g. 'projectile',
|
|
@@ -20,9 +19,10 @@ const SkillDefinitionSchema = new Schema(
|
|
|
20
19
|
);
|
|
21
20
|
|
|
22
21
|
/**
|
|
23
|
-
* Authoritative skill definition for a single trigger item. Owns the full skill
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* Authoritative skill definition for a single trigger item. Owns the full skill record — logic
|
|
23
|
+
* event keys plus the expanded metadata, including the summoned-entity item ids the simulation
|
|
24
|
+
* needs. The collection is deployment-wide: which of these an instance runs is derived from the
|
|
25
|
+
* content that instance names, never stored on it. See cyberia-instance-items.js.
|
|
26
26
|
*/
|
|
27
27
|
const CyberiaSkillSchema = new Schema(
|
|
28
28
|
{
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The File reference registry: which model fields hold a File `_id`.
|
|
3
|
+
*
|
|
4
|
+
* `file.ref.json` is the single source of that mapping — the orphan sweep in
|
|
5
|
+
* `underpost db --clean-fs` reads it, and so does every writer that deletes a
|
|
6
|
+
* document and has to take the blobs it owned with it. Reading it here rather
|
|
7
|
+
* than restating the fields at each call site is what keeps a newly added
|
|
8
|
+
* reference field from being cleaned up in one place and leaked in another.
|
|
9
|
+
*
|
|
10
|
+
* @module src/api/file/file.ref.js
|
|
11
|
+
* @namespace FileReferenceRegistry
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs-extra';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
// Resolved from this module rather than the working directory: the CLI, the
|
|
18
|
+
// server and the test runner each start somewhere different.
|
|
19
|
+
const REGISTRY_PATH = fileURLToPath(new URL('./file.ref.json', import.meta.url));
|
|
20
|
+
|
|
21
|
+
let registry = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @method fileRefRegistry
|
|
25
|
+
* @description The parsed registry, read once per process.
|
|
26
|
+
* @returns {Array<{api: string, model: Object}>} Registry entries.
|
|
27
|
+
* @memberof FileReferenceRegistry
|
|
28
|
+
*/
|
|
29
|
+
const fileRefRegistry = () => (registry ??= JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8')));
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @method fileRefApis
|
|
33
|
+
* @description Every api that references File.
|
|
34
|
+
* @returns {string[]} Api names.
|
|
35
|
+
* @memberof FileReferenceRegistry
|
|
36
|
+
*/
|
|
37
|
+
const fileRefApis = () => fileRefRegistry().map((entry) => entry.api);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @method fileRefFields
|
|
41
|
+
* @description The File-referencing fields of one api, as dot paths.
|
|
42
|
+
* @param {string} api - Api name, as registered.
|
|
43
|
+
* @returns {string[]} Field paths, empty for an api that references no File.
|
|
44
|
+
* @memberof FileReferenceRegistry
|
|
45
|
+
*/
|
|
46
|
+
const fileRefFields = (api) => {
|
|
47
|
+
const walk = (node, prefix) =>
|
|
48
|
+
Object.entries(node ?? {}).flatMap(([field, value]) => {
|
|
49
|
+
const path = prefix ? `${prefix}.${field}` : field;
|
|
50
|
+
if (value === true) return [path];
|
|
51
|
+
return value && typeof value === 'object' ? walk(value, path) : [];
|
|
52
|
+
});
|
|
53
|
+
return walk(fileRefRegistry().find((entry) => entry.api === api)?.model, '');
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @method documentFileIds
|
|
58
|
+
* @description Collects the File ids a set of documents holds, as strings.
|
|
59
|
+
* @param {Array<Object>} docs - Documents, lean or hydrated.
|
|
60
|
+
* @param {string[]} fields - Field paths to read, as {@link fileRefFields} returns them.
|
|
61
|
+
* @returns {string[]} Unique ids.
|
|
62
|
+
* @memberof FileReferenceRegistry
|
|
63
|
+
*/
|
|
64
|
+
const documentFileIds = (docs, fields) => {
|
|
65
|
+
const ids = new Set();
|
|
66
|
+
const read = (doc, path) => path.split('.').reduce((value, part) => value?.[part], doc);
|
|
67
|
+
for (const doc of docs ?? []) {
|
|
68
|
+
for (const field of fields) {
|
|
69
|
+
const value = read(doc, field);
|
|
70
|
+
if (!value) continue;
|
|
71
|
+
if (Array.isArray(value)) value.forEach((entry) => entry && ids.add(String(entry)));
|
|
72
|
+
else ids.add(String(value));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return [...ids];
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @method deleteOwnedFiles
|
|
80
|
+
* @description Deletes the File documents an owner collection no longer points at.
|
|
81
|
+
*
|
|
82
|
+
* An id still held by a surviving owner document is kept: renders are content
|
|
83
|
+
* addressed, so two documents describing the same item resolve to the same File
|
|
84
|
+
* id, and dropping one of them must not blind the other.
|
|
85
|
+
* @param {Object} params
|
|
86
|
+
* @param {import('mongoose').Model} params.File - File model.
|
|
87
|
+
* @param {import('mongoose').Model} params.Owner - The collection that owns these ids.
|
|
88
|
+
* @param {string[]} params.fields - Owner fields holding File ids.
|
|
89
|
+
* @param {Array<*>} params.ids - Candidate ids, as read before the owners were deleted.
|
|
90
|
+
* @returns {Promise<number>} How many File documents were removed.
|
|
91
|
+
* @memberof FileReferenceRegistry
|
|
92
|
+
*/
|
|
93
|
+
const deleteOwnedFiles = async ({ File, Owner, fields, ids }) => {
|
|
94
|
+
const candidates = [...new Set((ids ?? []).filter(Boolean).map(String))];
|
|
95
|
+
if (candidates.length === 0) return 0;
|
|
96
|
+
|
|
97
|
+
const held = new Set();
|
|
98
|
+
if (Owner && fields?.length) {
|
|
99
|
+
const survivors = await Owner.find(
|
|
100
|
+
{ $or: fields.map((field) => ({ [field]: { $in: candidates } })) },
|
|
101
|
+
Object.fromEntries(fields.map((field) => [field, 1])),
|
|
102
|
+
).lean();
|
|
103
|
+
for (const id of documentFileIds(survivors, fields)) held.add(id);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const removable = candidates.filter((id) => !held.has(id));
|
|
107
|
+
if (removable.length === 0) return 0;
|
|
108
|
+
const { deletedCount } = await File.deleteMany({ _id: { $in: removable } });
|
|
109
|
+
return deletedCount ?? 0;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @method deleteUnreferencedFiles
|
|
114
|
+
* @description Deletes the candidates no registered model references any more.
|
|
115
|
+
*
|
|
116
|
+
* Refuses to delete anything while a registered api has no model in this
|
|
117
|
+
* connection: an unreadable collection cannot be shown to hold no reference,
|
|
118
|
+
* and a sweep that assumed otherwise would delete a live blob of another domain.
|
|
119
|
+
* @param {Object} params
|
|
120
|
+
* @param {import('mongoose').Model} params.File - File model.
|
|
121
|
+
* @param {Array<*>} params.ids - Candidate ids.
|
|
122
|
+
* @param {function(string): import('mongoose').Model} params.getModel - Resolves an api to its model, throwing when it is not loaded.
|
|
123
|
+
* @returns {Promise<{removed: number, candidates: number, skipped: string}>} `skipped` names the api that could not be read, or is empty.
|
|
124
|
+
* @memberof FileReferenceRegistry
|
|
125
|
+
*/
|
|
126
|
+
const deleteUnreferencedFiles = async ({ File, ids, getModel }) => {
|
|
127
|
+
const candidates = [...new Set((ids ?? []).filter(Boolean).map(String))];
|
|
128
|
+
if (candidates.length === 0) return { removed: 0, candidates: 0, skipped: '' };
|
|
129
|
+
|
|
130
|
+
const referenced = new Set();
|
|
131
|
+
for (const api of fileRefApis()) {
|
|
132
|
+
const fields = fileRefFields(api);
|
|
133
|
+
if (fields.length === 0) continue;
|
|
134
|
+
let Model;
|
|
135
|
+
try {
|
|
136
|
+
Model = getModel(api);
|
|
137
|
+
} catch {
|
|
138
|
+
return { removed: 0, candidates: candidates.length, skipped: api };
|
|
139
|
+
}
|
|
140
|
+
const docs = await Model.find(
|
|
141
|
+
{ $or: fields.map((field) => ({ [field]: { $in: candidates } })) },
|
|
142
|
+
Object.fromEntries(fields.map((field) => [field, 1])),
|
|
143
|
+
).lean();
|
|
144
|
+
for (const id of documentFileIds(docs, fields)) referenced.add(id);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const removable = candidates.filter((id) => !referenced.has(id));
|
|
148
|
+
if (removable.length === 0) return { removed: 0, candidates: candidates.length, skipped: '' };
|
|
149
|
+
const { deletedCount } = await File.deleteMany({ _id: { $in: removable } });
|
|
150
|
+
return { removed: deletedCount ?? 0, candidates: candidates.length, skipped: '' };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export { fileRefRegistry, fileRefApis, fileRefFields, documentFileIds, deleteOwnedFiles, deleteUnreferencedFiles };
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
{
|
|
3
3
|
"api": "atlas-sprite-sheet",
|
|
4
4
|
"model": {
|
|
5
|
-
"fileId": true
|
|
5
|
+
"fileId": true,
|
|
6
|
+
"minifyFileId": true,
|
|
7
|
+
"idlePreviewFileId": true
|
|
6
8
|
}
|
|
7
9
|
},
|
|
8
10
|
{
|
|
@@ -11,6 +13,12 @@
|
|
|
11
13
|
"logo": true
|
|
12
14
|
}
|
|
13
15
|
},
|
|
16
|
+
{
|
|
17
|
+
"api": "cyberia-audio",
|
|
18
|
+
"model": {
|
|
19
|
+
"fileId": true
|
|
20
|
+
}
|
|
21
|
+
},
|
|
14
22
|
{
|
|
15
23
|
"api": "cyberia-instance",
|
|
16
24
|
"model": {
|
|
@@ -20,7 +28,8 @@
|
|
|
20
28
|
{
|
|
21
29
|
"api": "cyberia-map",
|
|
22
30
|
"model": {
|
|
23
|
-
"thumbnail": true
|
|
31
|
+
"thumbnail": true,
|
|
32
|
+
"preview": true
|
|
24
33
|
}
|
|
25
34
|
},
|
|
26
35
|
{
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* @namespace CyberiaObjectLayerModel
|
|
5
5
|
*/
|
|
6
6
|
import crypto from 'crypto';
|
|
7
|
+
import { STAT_TYPES, STAT_DEFAULT, STAT_MODIFIER_MIN, STAT_MODIFIER_MAX, STAT_CONTRACT_VERSION, validateStats } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
|
|
7
8
|
import stringify from 'fast-json-stable-stringify';
|
|
8
9
|
import { Schema, model } from 'mongoose';
|
|
10
|
+
import { deleteOwnedFiles, documentFileIds, fileRefFields } from '../file/file.ref.js';
|
|
9
11
|
/**
|
|
10
12
|
* @typedef {Object} Stats
|
|
11
13
|
* @property {number} effect - The effect attribute value
|
|
@@ -17,15 +19,11 @@ import { Schema, model } from 'mongoose';
|
|
|
17
19
|
* @memberof CyberiaObjectLayerModel
|
|
18
20
|
*/
|
|
19
21
|
const StatsSchema = new Schema(
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
intelligence: { type: Number, required: true, min: 0 },
|
|
26
|
-
utility: { type: Number, required: true, min: 0 },
|
|
27
|
-
},
|
|
28
|
-
{ _id: false },
|
|
22
|
+
Object.fromEntries(STAT_TYPES.map((key) => [key, {
|
|
23
|
+
type: Number, required: true, default: STAT_DEFAULT, min: STAT_MODIFIER_MIN, max: STAT_MODIFIER_MAX,
|
|
24
|
+
validate: Number.isInteger,
|
|
25
|
+
}])),
|
|
26
|
+
{ _id: false, strict: 'throw' },
|
|
29
27
|
);
|
|
30
28
|
/**
|
|
31
29
|
* @typedef {Object} Item
|
|
@@ -112,6 +110,7 @@ const RenderSchema = new Schema(
|
|
|
112
110
|
*/
|
|
113
111
|
const ObjectLayerSchema = new Schema(
|
|
114
112
|
{
|
|
113
|
+
statContractVersion: { type: Number, default: STAT_CONTRACT_VERSION, enum: [STAT_CONTRACT_VERSION] },
|
|
115
114
|
data: {
|
|
116
115
|
stats: { type: StatsSchema, required: true },
|
|
117
116
|
item: { type: ItemSchema, required: true },
|
|
@@ -228,7 +227,7 @@ function mergeObjectLayerData(existing, incoming) {
|
|
|
228
227
|
* payload the writer hashed before merging.
|
|
229
228
|
* @memberof CyberiaObjectLayerModel
|
|
230
229
|
*/
|
|
231
|
-
const UPSERTABLE_FIELDS = ['data', 'cid', 'objectLayerRenderFramesId', 'atlasSpriteSheetId'];
|
|
230
|
+
const UPSERTABLE_FIELDS = ['statContractVersion', 'data', 'cid', 'objectLayerRenderFramesId', 'atlasSpriteSheetId'];
|
|
232
231
|
|
|
233
232
|
/**
|
|
234
233
|
* Collapses any pre-existing duplicates of a given `data.item.id` down to a
|
|
@@ -247,12 +246,47 @@ async function collapseItemIdDuplicates(Model, itemId) {
|
|
|
247
246
|
const removedIds = duplicates.map((duplicate) => duplicate._id);
|
|
248
247
|
await Model.deleteMany({ _id: { $in: removedIds } });
|
|
249
248
|
|
|
249
|
+
// What the survivor still links is never collateral: a duplicate written by an older
|
|
250
|
+
// upsert can carry the very same render frames or atlas the survivor points at.
|
|
251
|
+
const survivorLinks = new Set(
|
|
252
|
+
[survivor.objectLayerRenderFramesId, survivor.atlasSpriteSheetId].filter(Boolean).map(String),
|
|
253
|
+
);
|
|
254
|
+
const droppedLinks = (field) =>
|
|
255
|
+
[
|
|
256
|
+
...new Set(
|
|
257
|
+
duplicates
|
|
258
|
+
.map((duplicate) => duplicate[field])
|
|
259
|
+
.filter(Boolean)
|
|
260
|
+
.map(String),
|
|
261
|
+
),
|
|
262
|
+
].filter((id) => !survivorLinks.has(id));
|
|
263
|
+
|
|
250
264
|
// Orphaned render frames have no other cleanup path once their owner is gone.
|
|
251
|
-
const renderFramesIds =
|
|
265
|
+
const renderFramesIds = droppedLinks('objectLayerRenderFramesId');
|
|
252
266
|
if (renderFramesIds.length > 0 && Model.db.models.ObjectLayerRenderFrames) {
|
|
253
267
|
await Model.db.models.ObjectLayerRenderFrames.deleteMany({ _id: { $in: renderFramesIds } });
|
|
254
268
|
}
|
|
255
269
|
|
|
270
|
+
// An atlas owns File renders, so dropping its document without them is what leaves
|
|
271
|
+
// unreachable blobs in the File collection.
|
|
272
|
+
const atlasIds = droppedLinks('atlasSpriteSheetId');
|
|
273
|
+
const AtlasSpriteSheet = Model.db.models.AtlasSpriteSheet;
|
|
274
|
+
if (atlasIds.length > 0 && AtlasSpriteSheet) {
|
|
275
|
+
const fields = fileRefFields('atlas-sprite-sheet');
|
|
276
|
+
const atlasDocs = await AtlasSpriteSheet.find(
|
|
277
|
+
{ _id: { $in: atlasIds } },
|
|
278
|
+
Object.fromEntries(fields.map((field) => [field, 1])),
|
|
279
|
+
).lean();
|
|
280
|
+
await AtlasSpriteSheet.deleteMany({ _id: { $in: atlasIds } });
|
|
281
|
+
if (Model.db.models.File)
|
|
282
|
+
await deleteOwnedFiles({
|
|
283
|
+
File: Model.db.models.File,
|
|
284
|
+
Owner: AtlasSpriteSheet,
|
|
285
|
+
fields,
|
|
286
|
+
ids: documentFileIds(atlasDocs, fields),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
256
290
|
return { survivor, removedIds };
|
|
257
291
|
}
|
|
258
292
|
|
|
@@ -332,6 +366,7 @@ ObjectLayerSchema.statics.upsertByItemId = async function (payload, { setOnInser
|
|
|
332
366
|
const itemId = payload?.data?.item?.id;
|
|
333
367
|
if (!itemId) throw new Error('ObjectLayer.upsertByItemId requires data.item.id');
|
|
334
368
|
|
|
369
|
+
if (Object.hasOwn(payload.data, 'stats')) validateStats(payload.data.stats);
|
|
335
370
|
const { survivor } = await collapseItemIdDuplicates(this, itemId);
|
|
336
371
|
|
|
337
372
|
if (!survivor) {
|
|
@@ -348,7 +383,7 @@ ObjectLayerSchema.statics.upsertByItemId = async function (payload, { setOnInser
|
|
|
348
383
|
}
|
|
349
384
|
update.sha256 = computeObjectLayerSha256(update.data ?? existing.data);
|
|
350
385
|
|
|
351
|
-
return await this.findByIdAndUpdate(survivor._id, { $set: update }, { returnDocument: 'after' });
|
|
386
|
+
return await this.findByIdAndUpdate(survivor._id, { $set: update }, { returnDocument: 'after', runValidators: true });
|
|
352
387
|
};
|
|
353
388
|
|
|
354
389
|
// Create and export the model
|
|
@@ -347,8 +347,8 @@ class ObjectLayerRouter {
|
|
|
347
347
|
/*
|
|
348
348
|
#swagger.auto = false
|
|
349
349
|
#swagger.tags = ['object-layer']
|
|
350
|
-
#swagger.summary = '
|
|
351
|
-
#swagger.description = 'Returns partial matches of data.item.id
|
|
350
|
+
#swagger.summary = 'Resolve object layer item identity'
|
|
351
|
+
#swagger.description = 'Returns { id, type } for partial matches of data.item.id (type-ahead) or for an exact id list'
|
|
352
352
|
#swagger.path = '/object-layer/search-item-ids'
|
|
353
353
|
#swagger.method = 'get'
|
|
354
354
|
#swagger.produces = ['application/json']
|
|
@@ -360,8 +360,15 @@ class ObjectLayerRouter {
|
|
|
360
360
|
type: 'string'
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
+
#swagger.parameters['ids'] = {
|
|
364
|
+
in: 'query',
|
|
365
|
+
description: 'Comma separated exact item IDs to resolve',
|
|
366
|
+
required: false,
|
|
367
|
+
type: 'string'
|
|
368
|
+
}
|
|
369
|
+
|
|
363
370
|
#swagger.responses[200] = {
|
|
364
|
-
description: 'Item
|
|
371
|
+
description: 'Item identities matching the query',
|
|
365
372
|
content: {
|
|
366
373
|
'application/json': {
|
|
367
374
|
schema: {
|