Compare commits

...

11 Commits

Author SHA1 Message Date
CI
ae50070e70 chore: release version v1.99.1 2026-04-15 09:09:13 +00:00
Dmitry Popov
6e9bbd231f Merge pull request #605 from wanderer-industries/passage-mass-2
fix: add lost passage mass files
2026-04-15 11:08:48 +02:00
DanSylvest
a86d47ac04 fix: add lost passage mass files 2026-04-15 08:49:26 +03:00
CI
34f1d0fa8d chore: [skip ci] 2026-04-14 11:33:28 +00:00
CI
39b1d09753 chore: release version v1.99.0 2026-04-14 11:33:28 +00:00
Dmitry Popov
3d675aa10c Merge pull request #604 from wanderer-industries/passages-mass
Passages mass
2026-04-14 13:32:55 +02:00
Dmitry Popov
cb66b73337 Merge branch 'main' into passages-mass 2026-04-13 11:00:25 +02:00
DanSylvest
22ae774127 fix: Add update mass button for ship 2026-04-12 09:23:32 +03:00
CI
ab41b38f12 chore: [skip ci] 2026-04-10 21:58:21 +00:00
DanSylvest
3f9eff0d33 chore: add btn for passage edit 2026-04-03 11:04:14 +03:00
Dmitry Popov
8e5ed22bc0 feat(core): Add support for editing passages mass 2026-04-01 11:55:25 +02:00
15 changed files with 828 additions and 43 deletions

View File

@@ -2,6 +2,28 @@
<!-- changelog -->
## [v1.99.1](https://github.com/wanderer-industries/wanderer/compare/v1.99.0...v1.99.1) (2026-04-15)
### Bug Fixes:
* add lost passage mass files
## [v1.99.0](https://github.com/wanderer-industries/wanderer/compare/v1.98.1...v1.99.0) (2026-04-14)
### Features:
* core: Add support for editing passages mass
### Bug Fixes:
* Add update mass button for ship
## [v1.98.1](https://github.com/wanderer-industries/wanderer/compare/v1.98.0...v1.98.1) (2026-04-10)

View File

@@ -12,7 +12,7 @@ export const LocalCharactersItemTemplate = ({ showShipName, ...options }: LocalC
<div
className={clsx(
classes.CharacterRow,
'box-border flex items-center w-full whitespace-nowrap overflow-hidden text-ellipsis min-w-[0px]',
'box-border flex items-center w-full whitespace-nowrap overflow-hidden text-ellipsis min-w-[0px] ',
'px-1',
{
'surface-hover': options.odd,

View File

@@ -17,29 +17,35 @@ import classes from './Connections.module.scss';
import { InfoDrawer, SystemView, TimeAgo } from '@/hooks/Mapper/components/ui-kit';
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
import { PassageCard } from './PassageCard';
import { PassageMassDialog } from './PassageMassDialog';
const sortByDate = (a: string, b: string) => new Date(a).getTime() - new Date(b).getTime();
const itemTemplate = (item: PassageWithSourceTarget, options: VirtualScrollerTemplateOptions) => {
return (
<div
className={clsx(classes.CharacterRow, 'w-full box-border', {
'surface-hover': options.odd,
['border-b border-gray-600 border-opacity-20']: !options.last,
['bg-green-500 hover:bg-green-700 transition duration-300 bg-opacity-10 hover:bg-opacity-10']: false,
})}
style={{ height: options.props.itemSize + 'px' }}
>
<PassageCard {...item} />
</div>
);
};
const getPassageMass = (passage: Passage) => passage.mass ?? parseInt(passage.ship.ship_type_info.mass);
export interface ConnectionPassagesContentProps {
passages: PassageWithSourceTarget[];
onEditPassage: (passage: PassageWithSourceTarget) => void;
}
export const ConnectionPassages = ({ passages = [] }: ConnectionPassagesContentProps) => {
export const ConnectionPassages = ({ passages = [], onEditPassage }: ConnectionPassagesContentProps) => {
const itemTemplate = useCallback(
(item: PassageWithSourceTarget, options: VirtualScrollerTemplateOptions) => {
return (
<div
className={clsx(classes.CharacterRow, 'w-full box-border', {
'surface-hover': options.odd,
['border-b border-gray-600 border-opacity-20']: !options.last,
['bg-green-500 hover:bg-green-700 transition duration-300 bg-opacity-10 hover:bg-opacity-10']: false,
})}
style={{ height: options.props.itemSize + 'px' }}
>
<PassageCard {...item} onEdit={() => onEditPassage(item)} />
</div>
);
},
[onEditPassage],
);
if (passages.length === 0) {
return <div className="flex justify-center items-center text-stone-400 select-none">Nobody passed here</div>;
}
@@ -83,6 +89,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
const [passages, setPassages] = useState<Passage[]>([]);
const [info, setInfo] = useState<ConnectionInfoOutput | null>(null);
const [editingPassage, setEditingPassage] = useState<PassageWithSourceTarget | null>(null);
const loadInfo = useCallback(
async (connection: SolarSystemConnection) => {
@@ -109,7 +116,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
},
});
setPassages(result.passages.sort((a, b) => sortByDate(b.inserted_at, a.inserted_at)));
setPassages([...result.passages].sort((a, b) => sortByDate(b.inserted_at, a.inserted_at)));
},
[outCommand],
);
@@ -119,7 +126,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
return [];
}
return passages
return [...passages]
.sort((a, b) => sortByDate(b.inserted_at, a.inserted_at))
.map<PassageWithSourceTarget>(x => ({
...x,
@@ -130,16 +137,48 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
useEffect(() => {
if (!selectedConnection) {
setEditingPassage(null);
return;
}
setEditingPassage(null);
loadInfo(selectedConnection);
loadPassages(selectedConnection);
}, [selectedConnection]);
}, [loadInfo, loadPassages, selectedConnection]);
const approximateMass = useMemo(() => {
return passages.reduce((acc, x) => acc + parseInt(x.ship.ship_type_info.mass), 0);
return passages.reduce((acc, x) => acc + getPassageMass(x), 0);
}, [passages]);
const handleEditPassage = useCallback((passage: PassageWithSourceTarget) => {
setEditingPassage(passage);
}, []);
const handleHidePassageDialog = useCallback(() => {
setEditingPassage(null);
}, []);
const handleSavePassageMass = useCallback(
async (mass: number) => {
if (!editingPassage) {
return;
}
await outCommand({
type: OutCommand.updatePassageMass,
data: {
id: editingPassage.id,
mass,
},
});
setPassages(prev => prev.map(passage => (passage.id === editingPassage.id ? { ...passage, mass } : passage)));
setEditingPassage(prev => (prev ? { ...prev, mass } : prev));
handleHidePassageDialog();
},
[editingPassage, handleHidePassageDialog, outCommand],
);
if (!cnInfo) {
return null;
}
@@ -201,8 +240,15 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
{/* separator */}
<div className="w-full h-px bg-neutral-800 px-0.5"></div>
<ConnectionPassages passages={preparedPassages} />
<ConnectionPassages passages={preparedPassages} onEditPassage={handleEditPassage} />
</div>
<PassageMassDialog
passage={editingPassage}
visible={editingPassage != null}
onHide={handleHidePassageDialog}
onSave={handleSavePassageMass}
/>
</Sidebar>
);
};

View File

@@ -1,34 +1,34 @@
import clsx from 'clsx';
import classes from './PassageCard.module.scss';
import { PassageWithSourceTarget } from '@/hooks/Mapper/types';
import { SystemView, TimeAgo, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { SystemView, TimeAgo, TooltipPosition, WdImgButton, WdTransition } from '@/hooks/Mapper/components/ui-kit';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
import { useCallback, useMemo } from 'react';
import { MouseEvent, useCallback, useMemo, useState } from 'react';
import { ZKB_ICON } from '@/hooks/Mapper/icons';
import { charEveWhoLink, charZKBLink } from '@/hooks/Mapper/helpers/linkHelpers.ts';
import { getShipName } from './getShipName.ts';
type PassageCardType = {
// compact?: boolean;
showShipName?: boolean;
// showSystem?: boolean;
// useSystemsCache?: boolean;
onEdit?: () => void;
} & PassageWithSourceTarget;
const SHIP_NAME_RX = /u'|'/g;
export const getShipName = (name: string) => {
return name
.replace(SHIP_NAME_RX, '')
.replace(/\\u([\dA-Fa-f]{4})/g, (_, grp) => {
return String.fromCharCode(parseInt(grp, 16));
})
.replace(/\\x([\dA-Fa-f]{2})/g, (_, grp) => {
return String.fromCharCode(parseInt(grp, 16));
});
};
export const PassageCard = ({ inserted_at, character: char, ship, source, target, from }: PassageCardType) => {
export const PassageCard = ({
inserted_at,
character: char,
ship,
source,
target,
from,
mass,
onEdit,
}: PassageCardType) => {
const isOwn = false;
const [hovered, setHovered] = useState(false);
const insertedAt = useMemo(() => {
const date = new Date(inserted_at);
@@ -37,9 +37,23 @@ export const PassageCard = ({ inserted_at, character: char, ship, source, target
const handleOpenZKB = useCallback(() => window.open(charZKBLink(char.eve_id), '_blank'), [char]);
const handleOpenEveWho = useCallback(() => window.open(charEveWhoLink(char.eve_id), '_blank'), [char]);
const handleEdit = useCallback(
(event: MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
onEdit?.();
},
[onEdit],
);
const handleMouseEnter = useCallback(() => setHovered(true), []);
const handleMouseLeave = useCallback(() => setHovered(false), []);
return (
<div className={clsx(classes.CharacterCard, 'w-full text-xs', 'flex flex-col box-border')}>
<div
className={clsx(classes.CharacterCard, 'w-full text-xs', 'flex flex-col box-border')}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className="flex flex-col justify-between px-2 py-1 gap-1">
{/*here icon and other*/}
<div className={clsx(classes.CharRow, classes.FourColumns)}>
@@ -140,15 +154,30 @@ export const PassageCard = ({ inserted_at, character: char, ship, source, target
</WdTooltipWrapper>
</span>
<div className="text-stone-400">{kgToTons(parseInt(ship.ship_type_info.mass))}</div>
<div className="text-stone-400">{kgToTons(mass ?? parseInt(ship.ship_type_info.mass))}</div>
</div>
</div>
{/*ship icon*/}
<span
<div
className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')}
style={{ backgroundImage: `url(https://images.evetech.net/types/${ship.ship_type_id}/icon)` }}
/>
>
<WdTransition active={hovered} timeout={50}>
<div>
{hovered && (
<div
className={clsx(
'transition-all transform ease-in duration-200 cursor-pointer',
'pi text-stone-500 text-[17px] w-[33px] h-[32px] !flex items-center justify-center border rounded-[2px]',
'pi-cog text-stone-200/80 hover:!text-orange-400 border-stone-500/70 hover:!border-orange-400 bg-stone-800/70',
)}
onClick={handleEdit}
/>
)}
</div>
</WdTransition>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
const SHIP_NAME_RX = /u'|'/g;
export const getShipName = (name: string) => {
return name
.replace(SHIP_NAME_RX, '')
.replace(/\\u([\dA-Fa-f]{4})/g, (_, grp) => {
return String.fromCharCode(parseInt(grp, 16));
})
.replace(/\\x([\dA-Fa-f]{2})/g, (_, grp) => {
return String.fromCharCode(parseInt(grp, 16));
});
};

View File

@@ -0,0 +1,140 @@
import { WdButton, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { PassageWithSourceTarget } from '@/hooks/Mapper/types';
import { Dialog } from 'primereact/dialog';
import { InputText } from 'primereact/inputtext';
import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react';
import { TimeAgo } from '@/hooks/Mapper/components/ui-kit';
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
import { getShipName } from './PassageCard/getShipName.ts';
type PassageMassDialogProps = {
passage: PassageWithSourceTarget | null;
visible: boolean;
onHide: () => void;
onSave: (mass: number) => Promise<void> | void;
};
const getPassageMass = (passage: PassageWithSourceTarget) => {
return passage.mass ?? parseInt(passage.ship.ship_type_info.mass);
};
const parseMassValue = (value: string) => {
const sanitized = value.replace(/[^\d]/g, '');
if (sanitized === '') {
return null;
}
const parsed = parseInt(sanitized);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
};
export const PassageMassDialog = ({ passage, visible, onHide, onSave }: PassageMassDialogProps) => {
const [massValue, setMassValue] = useState('');
const [saving, setSaving] = useState(false);
useEffect(() => {
if (!passage) {
setMassValue('');
return;
}
setMassValue(`${getPassageMass(passage)}`);
}, [passage]);
const parsedMass = useMemo(() => parseMassValue(massValue), [massValue]);
const handleSave = async () => {
if (!passage || parsedMass == null) {
return;
}
setSaving(true);
try {
await onSave(parsedMass);
} finally {
setSaving(false);
}
};
return (
<Dialog
header="Edit passage mass"
visible={visible}
draggable
resizable={false}
style={{ width: '420px' }}
onHide={onHide}
>
{passage && (
<div className="flex flex-col gap-4">
<div className="rounded border border-stone-700/80 bg-stone-900/70 p-3">
<div className="grid grid-cols-[34px_1fr_auto] gap-3 items-start">
<div
className="w-[34px] h-[34px] rounded-[3px] border border-stone-700 bg-center bg-cover bg-no-repeat"
style={{ backgroundImage: `url(https://images.evetech.net/types/${passage.ship.ship_type_id}/icon)` }}
/>
<div className="min-w-0">
<div className="text-sm text-stone-100 truncate">{passage.ship.ship_type_info.name}</div>
{passage.ship.ship_name && (
<div className="text-xs text-stone-400 truncate">{getShipName(passage.ship.ship_name)}</div>
)}
<div className="mt-2 flex items-center gap-2 text-xs text-stone-400">
<span>{passage.character.name}</span>
<span className="text-stone-600">|</span>
<WdTooltipWrapper content={new Date(passage.inserted_at).toLocaleString()}>
<span className="cursor-default">
<TimeAgo timestamp={passage.inserted_at} />
</span>
</WdTooltipWrapper>
</div>
</div>
<div
className={clsx(
'w-[34px] h-[34px] rounded-[3px] border border-stone-700 bg-center bg-cover bg-no-repeat',
'justify-self-end',
)}
style={{
backgroundImage: `url(https://images.evetech.net/characters/${passage.character.eve_id}/portrait)`,
}}
/>
</div>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm text-stone-300" htmlFor="passage-mass">
Passage mass
</label>
<InputText
id="passage-mass"
value={massValue}
onChange={event => setMassValue(event.target.value.replace(/[^\d]/g, ''))}
placeholder="Mass in kg"
className="w-full"
/>
<div className="text-xs text-stone-500">
{parsedMass == null ? 'Enter mass in kg' : `Preview: ${kgToTons(parsedMass)}`}
</div>
</div>
<div className="flex justify-end gap-2">
<WdButton outlined size="small" label="Cancel" onClick={onHide} />
<WdButton
outlined
size="small"
label={saving ? 'Saving...' : 'Save'}
onClick={handleSave}
disabled={parsedMass == null || saving}
/>
</div>
</div>
)}
</Dialog>
);
};

View File

@@ -6,8 +6,10 @@ export type PassageLimitedCharacterType = Pick<
>;
export type Passage = {
id: string;
from: boolean;
inserted_at: string; // Date
mass: number | null;
ship: ShipTypeRaw;
character: PassageLimitedCharacterType;
};

View File

@@ -272,6 +272,7 @@ export enum OutCommand {
addSystem = 'add_system',
openUserSettings = 'open_user_settings',
getPassages = 'get_passages',
updatePassageMass = 'update_passage_mass',
linkSignatureToSystem = 'link_signature_to_system',
getCorporationNames = 'get_corporation_names',
getCorporationTicker = 'get_corporation_ticker',

View File

@@ -17,12 +17,15 @@ defmodule WandererApp.Api.MapChainPassages do
define(:read, action: :read)
define(:by_map_id, action: :by_map_id)
define(:by_connection, action: :by_connection)
define(:update_mass, action: :update_mass)
define(:by_id, get_by: [:id], action: :read)
end
actions do
default_accept [
:ship_type_id,
:ship_name,
:mass,
:solar_system_source_id,
:solar_system_target_id
]
@@ -30,6 +33,12 @@ defmodule WandererApp.Api.MapChainPassages do
defaults [:create, :read, :destroy]
update :update do
accept [:mass]
require_atomic? false
end
update :update_mass do
accept [:mass]
require_atomic? false
end
@@ -37,6 +46,7 @@ defmodule WandererApp.Api.MapChainPassages do
accept [
:ship_type_id,
:ship_name,
:mass,
:solar_system_source_id,
:solar_system_target_id,
:map_id,
@@ -85,8 +95,10 @@ defmodule WandererApp.Api.MapChainPassages do
|> WandererApp.Repo.all()
|> Enum.map(fn [passage, character] ->
%{
id: passage.id,
ship_type_id: passage.ship_type_id,
ship_name: passage.ship_name,
mass: passage.mass,
inserted_at: passage.inserted_at,
character: character
}
@@ -108,6 +120,7 @@ defmodule WandererApp.Api.MapChainPassages do
attribute :ship_type_id, :integer
attribute :ship_name, :string
attribute :mass, :integer
attribute :solar_system_source_id, :integer
attribute :solar_system_target_id, :integer

View File

@@ -265,6 +265,42 @@ defmodule WandererAppWeb.MapConnectionsEventHandler do
{:reply, passages, socket}
end
def handle_ui_event(
"update_passage_mass",
%{"id" => passage_id, "mass" => mass} = _event,
%{
assigns: %{
has_tracked_characters?: true,
user_permissions: %{update_system: true}
}
} = socket
) do
mass_value =
cond do
is_integer(mass) ->
mass
is_binary(mass) ->
case Integer.parse(mass) do
{int_val, _} -> int_val
:error -> nil
end
true ->
nil
end
case WandererApp.Api.MapChainPassages.by_id(passage_id) do
{:ok, passage} when not is_nil(passage) ->
WandererApp.Api.MapChainPassages.update_mass(passage, %{mass: mass_value})
_ ->
Logger.warning("update_passage_mass: passage not found id=#{passage_id}")
end
{:noreply, socket}
end
def handle_ui_event(event, body, socket),
do: MapCoreEventHandler.handle_ui_event(event, body, socket)

View File

@@ -88,7 +88,8 @@ defmodule WandererAppWeb.MapEventHandler do
"update_connection_mass_status",
"update_connection_ship_size_type",
"update_connection_locked",
"update_connection_custom_info"
"update_connection_custom_info",
"update_passage_mass"
]
@map_activity_events [

View File

@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
@source_url "https://github.com/wanderer-industries/wanderer"
@version "1.98.1"
@version "1.99.1"
def project do
[

View File

@@ -0,0 +1,29 @@
defmodule WandererApp.Repo.Migrations.AddMassToMapChainPassages do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:maps_v1) do
modify :scopes, {:array, :text}, default: '{wormholes}'
end
alter table(:map_chain_passages_v1) do
add :mass, :bigint
end
end
def down do
alter table(:map_chain_passages_v1) do
remove :mass
end
alter table(:maps_v1) do
modify :scopes, {:array, :text}, default: nil
end
end
end

View File

@@ -0,0 +1,177 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"precision": null,
"primary_key?": true,
"references": null,
"scale": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "ship_type_id",
"type": "bigint"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "ship_name",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "mass",
"type": "bigint"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "solar_system_source_id",
"type": "bigint"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "solar_system_target_id",
"type": "bigint"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "inserted_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "updated_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": true,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "map_chain_passages_v1_map_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": null,
"table": "maps_v1"
},
"scale": null,
"size": null,
"source": "map_id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": true,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "map_chain_passages_v1_character_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": null,
"table": "character_v1"
},
"scale": null,
"size": null,
"source": "character_id",
"type": "uuid"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "00AA9FB7759FCDF16C5C627E6735E0B568E517A360F2002AFE00018BD6CD8F2A",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.WandererApp.Repo",
"schema": null,
"table": "map_chain_passages_v1"
}

View File

@@ -0,0 +1,277 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"precision": null,
"primary_key?": true,
"references": null,
"scale": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "name",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "slug",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "description",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "personal_note",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "public_api_key",
"type": "text"
},
{
"allow_nil?": true,
"default": "[]",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "hubs",
"type": [
"array",
"text"
]
},
{
"allow_nil?": false,
"default": "\"wormholes\"",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "scope",
"type": "text"
},
{
"allow_nil?": true,
"default": "false",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "deleted",
"type": "boolean"
},
{
"allow_nil?": true,
"default": "false",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "only_tracked_characters",
"type": "boolean"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "options",
"type": "text"
},
{
"allow_nil?": false,
"default": "false",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "webhooks_enabled",
"type": "boolean"
},
{
"allow_nil?": false,
"default": "false",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "sse_enabled",
"type": "boolean"
},
{
"allow_nil?": true,
"default": "'{wormholes}'",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "scopes",
"type": [
"array",
"text"
]
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "inserted_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "updated_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "maps_v1_owner_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": null,
"table": "character_v1"
},
"scale": null,
"size": null,
"source": "owner_id",
"type": "uuid"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "21B2A84E49086754B40476C11B4EA5F576E8537449FB776941098773C5CD705F",
"identities": [
{
"all_tenants?": false,
"base_filter": null,
"index_name": "maps_v1_unique_public_api_key_index",
"keys": [
{
"type": "atom",
"value": "public_api_key"
}
],
"name": "unique_public_api_key",
"nils_distinct?": true,
"where": null
},
{
"all_tenants?": false,
"base_filter": null,
"index_name": "maps_v1_unique_slug_index",
"keys": [
{
"type": "atom",
"value": "slug"
}
],
"name": "unique_slug",
"nils_distinct?": true,
"where": null
}
],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.WandererApp.Repo",
"schema": null,
"table": "maps_v1"
}