wip(webui): use (new) attr constants
This commit is contained in:
parent
0eec69b219
commit
641f42f785
14 changed files with 79 additions and 31 deletions
31
tools/upend_js/constants.ts
Normal file
31
tools/upend_js/constants.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* Attribute denoting (hierarchical) relation, in the "upwards" direction. For example, a file `IN` a group, an image `IN` photos, etc.
|
||||||
|
*/
|
||||||
|
export const ATTR_IN = "IN";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute denoting that an entry belongs to the set relating to a given (hierarchical) relation.
|
||||||
|
* For example, a data blob may have a label entry, and to qualify that label within the context of belonging to a given hierarchical group, that label entry and the hierarchical entry will be linked with `BY`.
|
||||||
|
*/
|
||||||
|
export const ATTR_BY = "BY";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute denoting that an attribute belongs to a given "tagging" entity. If an entity belongs to (`IN`) a "tagging" entity, it is expected to have attributes that are `OF` that entity.
|
||||||
|
*/
|
||||||
|
export const ATTR_OF = "OF";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute denoting a human readable label.
|
||||||
|
*/
|
||||||
|
export const ATTR_LABEL = "LBL";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute denoting the date & time an entity was noted in the database.
|
||||||
|
* (TODO: This info can be trivially derived from existing entry timestamps, while at the same time the "Introduction problem" is still open.)
|
||||||
|
*/
|
||||||
|
export const ATTR_ADDED = "ADDED";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute for cross-vault unambiguous referencing of non-hashable (e.g. UUID) entities.
|
||||||
|
*/
|
||||||
|
export const ATTR_KEY = "KEY";
|
|
@ -19,6 +19,7 @@
|
||||||
import EntryList from "./widgets/EntryList.svelte";
|
import EntryList from "./widgets/EntryList.svelte";
|
||||||
import api from "../lib/api";
|
import api from "../lib/api";
|
||||||
import Gallery from "./widgets/Gallery.svelte";
|
import Gallery from "./widgets/Gallery.svelte";
|
||||||
|
import { ATTR_IN, ATTR_LABEL, ATTR_KEY } from "upend/constants";
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
|
@ -76,9 +77,9 @@
|
||||||
$: filteredUntypedAttributes = untypedAttributes.filter(
|
$: filteredUntypedAttributes = untypedAttributes.filter(
|
||||||
(entry) =>
|
(entry) =>
|
||||||
![
|
![
|
||||||
"LBL",
|
ATTR_LABEL,
|
||||||
"OF",
|
ATTR_IN,
|
||||||
"KEY",
|
ATTR_KEY,
|
||||||
"NOTE",
|
"NOTE",
|
||||||
"LAST_VISITED",
|
"LAST_VISITED",
|
||||||
"NUM_VISITED",
|
"NUM_VISITED",
|
||||||
|
@ -94,14 +95,14 @@
|
||||||
(editable
|
(editable
|
||||||
? $entity?.backlinks
|
? $entity?.backlinks
|
||||||
: $entity?.backlinks.filter(
|
: $entity?.backlinks.filter(
|
||||||
(entry) => !["OF"].includes(entry.attribute)
|
(entry) => ![ATTR_IN].includes(entry.attribute)
|
||||||
)) || [];
|
)) || [];
|
||||||
|
|
||||||
$: groups = ($entity?.attr["OF"] || []).map((e) => [
|
$: groups = ($entity?.attr[ATTR_IN] || []).map((e) => [
|
||||||
e.value.c as string,
|
e.value.c as string,
|
||||||
e.address,
|
e.address,
|
||||||
]);
|
]);
|
||||||
$: tagged = $entity?.attr["~OF"] || [];
|
$: tagged = $entity?.attr[`~${ATTR_IN}`] || [];
|
||||||
|
|
||||||
let attributesUsed: UpEntry[] = [];
|
let attributesUsed: UpEntry[] = [];
|
||||||
$: {
|
$: {
|
||||||
|
@ -149,7 +150,7 @@
|
||||||
await api.putEntry([
|
await api.putEntry([
|
||||||
{
|
{
|
||||||
entity: address,
|
entity: address,
|
||||||
attribute: "OF",
|
attribute: ATTR_IN,
|
||||||
value: {
|
value: {
|
||||||
t: "Address",
|
t: "Address",
|
||||||
c: String(groupToAdd.c),
|
c: String(groupToAdd.c),
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
import { getTypes } from "../../util/mediatypes";
|
import { getTypes } from "../../util/mediatypes";
|
||||||
import { formatDuration } from "../../util/fragments/time";
|
import { formatDuration } from "../../util/fragments/time";
|
||||||
import { concurrentImage } from "../imageQueue";
|
import { concurrentImage } from "../imageQueue";
|
||||||
|
import { ATTR_IN } from "upend/constants";
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let address: string;
|
export let address: string;
|
||||||
|
@ -38,14 +39,14 @@
|
||||||
let failedChildren: string[] = [];
|
let failedChildren: string[] = [];
|
||||||
let loadedChildren: string[] = [];
|
let loadedChildren: string[] = [];
|
||||||
$: groupChildren = $entity?.backlinks
|
$: groupChildren = $entity?.backlinks
|
||||||
.filter((e) => e.attribute === "OF")
|
.filter((e) => e.attribute === ATTR_IN)
|
||||||
.map((e) => String(e.entity))
|
.map((e) => String(e.entity))
|
||||||
.filter(
|
.filter(
|
||||||
(addr) =>
|
(addr) =>
|
||||||
!failedChildren
|
!failedChildren
|
||||||
.slice(
|
.slice(
|
||||||
0,
|
0,
|
||||||
$entity?.backlinks.filter((e) => e.attribute === "OF").length - 4
|
$entity?.backlinks.filter((e) => e.attribute === ATTR_IN).length - 4
|
||||||
)
|
)
|
||||||
.includes(addr)
|
.includes(addr)
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
import UpObject from "../../display/UpObject.svelte";
|
import UpObject from "../../display/UpObject.svelte";
|
||||||
import Spinner from "../../utils/Spinner.svelte";
|
import Spinner from "../../utils/Spinner.svelte";
|
||||||
import { i18n } from "../../../i18n";
|
import { i18n } from "../../../i18n";
|
||||||
|
import { ATTR_LABEL } from "upend/constants";
|
||||||
|
|
||||||
export let address: string;
|
export let address: string;
|
||||||
export let detail: boolean;
|
export let detail: boolean;
|
||||||
|
@ -51,7 +52,7 @@
|
||||||
color: annotation.get("COLOR") || DEFAULT_ANNOTATION_COLOR,
|
color: annotation.get("COLOR") || DEFAULT_ANNOTATION_COLOR,
|
||||||
attributes: {
|
attributes: {
|
||||||
"upend-address": annotation.address,
|
"upend-address": annotation.address,
|
||||||
label: annotation.get("LBL"),
|
label: annotation.get(ATTR_LABEL),
|
||||||
},
|
},
|
||||||
data: (annotation.attr["NOTE"] || [])[0]?.value,
|
data: (annotation.attr["NOTE"] || [])[0]?.value,
|
||||||
...fragment,
|
...fragment,
|
||||||
|
@ -92,7 +93,7 @@
|
||||||
} as any); // incorrect types, `update()` does take `attributes`
|
} as any); // incorrect types, `update()` does take `attributes`
|
||||||
}
|
}
|
||||||
|
|
||||||
await api.putEntityAttribute(entity, "LBL", {
|
await api.putEntityAttribute(entity, ATTR_LABEL, {
|
||||||
t: "String",
|
t: "String",
|
||||||
c: region.attributes["label"],
|
c: region.attributes["label"],
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import IconButton from "../../utils/IconButton.svelte";
|
import IconButton from "../../utils/IconButton.svelte";
|
||||||
import Spinner from "../../utils/Spinner.svelte";
|
import Spinner from "../../utils/Spinner.svelte";
|
||||||
import UpObject from "../UpObject.svelte";
|
import UpObject from "../UpObject.svelte";
|
||||||
|
import { ATTR_LABEL } from "upend/constants";
|
||||||
|
|
||||||
export let address: string;
|
export let address: string;
|
||||||
export let editable: boolean;
|
export let editable: boolean;
|
||||||
|
@ -58,7 +59,7 @@
|
||||||
if (annotation.get("W3C_FRAGMENT_SELECTOR")) {
|
if (annotation.get("W3C_FRAGMENT_SELECTOR")) {
|
||||||
anno.addAnnotation({
|
anno.addAnnotation({
|
||||||
type: "Annotation",
|
type: "Annotation",
|
||||||
body: annotation.attr["LBL"].map((e) => {
|
body: annotation.attr[ATTR_LABEL].map((e) => {
|
||||||
return {
|
return {
|
||||||
type: "TextualBody",
|
type: "TextualBody",
|
||||||
value: String(e.value.c),
|
value: String(e.value.c),
|
||||||
|
@ -134,7 +135,7 @@
|
||||||
...annotation.body.map((body) => {
|
...annotation.body.map((body) => {
|
||||||
return {
|
return {
|
||||||
entity: uuid,
|
entity: uuid,
|
||||||
attribute: "LBL",
|
attribute: ATTR_LABEL,
|
||||||
value: {
|
value: {
|
||||||
t: "String",
|
t: "String",
|
||||||
c: body.value,
|
c: body.value,
|
||||||
|
@ -146,9 +147,9 @@
|
||||||
anno.on("updateAnnotation", async (annotation) => {
|
anno.on("updateAnnotation", async (annotation) => {
|
||||||
const annotationObject = await api.fetchEntity(annotation.id);
|
const annotationObject = await api.fetchEntity(annotation.id);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
annotationObject.attr["LBL"]
|
annotationObject.attr[ATTR_LABEL].concat(
|
||||||
.concat(annotationObject.attr["W3C_FRAGMENT_SELECTOR"])
|
annotationObject.attr["W3C_FRAGMENT_SELECTOR"]
|
||||||
.map(async (e) => api.deleteEntry(e.address))
|
).map(async (e) => api.deleteEntry(e.address))
|
||||||
);
|
);
|
||||||
await api.putEntry([
|
await api.putEntry([
|
||||||
{
|
{
|
||||||
|
@ -162,7 +163,7 @@
|
||||||
...annotation.body.map((body) => {
|
...annotation.body.map((body) => {
|
||||||
return {
|
return {
|
||||||
entity: annotation.id,
|
entity: annotation.id,
|
||||||
attribute: "LBL",
|
attribute: ATTR_LABEL,
|
||||||
value: {
|
value: {
|
||||||
t: "String",
|
t: "String",
|
||||||
c: body.value,
|
c: body.value,
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
import { matchSorter } from "match-sorter";
|
import { matchSorter } from "match-sorter";
|
||||||
import api from "../../lib/api";
|
import api from "../../lib/api";
|
||||||
|
import { ATTR_LABEL } from "upend/constants";
|
||||||
|
|
||||||
const MAX_OPTIONS = 25;
|
const MAX_OPTIONS = 25;
|
||||||
|
|
||||||
|
@ -150,7 +151,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const validOptions = searchResult.entries
|
const validOptions = searchResult.entries
|
||||||
.filter((e) => e.attribute === "LBL")
|
.filter((e) => e.attribute === ATTR_LABEL)
|
||||||
.filter((e) => !exactHits.includes(e.entity));
|
.filter((e) => !exactHits.includes(e.entity));
|
||||||
|
|
||||||
const sortedOptions = matchSorter(validOptions, inputValue, {
|
const sortedOptions = matchSorter(validOptions, inputValue, {
|
||||||
|
@ -197,7 +198,7 @@
|
||||||
entity: { t: "Attribute", c: option.attribute.name },
|
entity: { t: "Attribute", c: option.attribute.name },
|
||||||
});
|
});
|
||||||
// Second, label it.
|
// Second, label it.
|
||||||
await api.putEntityAttribute(address, "LBL", {
|
await api.putEntityAttribute(address, ATTR_LABEL, {
|
||||||
t: "String",
|
t: "String",
|
||||||
c: option.labelToCreate,
|
c: option.labelToCreate,
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
import { formatDuration } from "../../util/fragments/time";
|
import { formatDuration } from "../../util/fragments/time";
|
||||||
import { i18n } from "../../i18n";
|
import { i18n } from "../../i18n";
|
||||||
import UpLink from "../display/UpLink.svelte";
|
import UpLink from "../display/UpLink.svelte";
|
||||||
|
import { ATTR_ADDED, ATTR_LABEL } from "upend/constants";
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let columns: string | undefined = undefined;
|
export let columns: string | undefined = undefined;
|
||||||
|
@ -87,7 +88,9 @@
|
||||||
|
|
||||||
const addressesString = addresses.map((addr) => `@${addr}`).join(" ");
|
const addressesString = addresses.map((addr) => `@${addr}`).join(" ");
|
||||||
|
|
||||||
labelListing = query(`(matches (in ${addressesString}) "LBL" ? )`).result;
|
labelListing = query(
|
||||||
|
`(matches (in ${addressesString}) "${ATTR_LABEL}" ? )`
|
||||||
|
).result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sorting
|
// Sorting
|
||||||
|
@ -159,7 +162,7 @@
|
||||||
switch (attribute) {
|
switch (attribute) {
|
||||||
case "FILE_SIZE":
|
case "FILE_SIZE":
|
||||||
return filesize(parseInt(String(value), 10), { base: 2 });
|
return filesize(parseInt(String(value), 10), { base: 2 });
|
||||||
case "ADDED":
|
case ATTR_ADDED:
|
||||||
case "LAST_VISITED":
|
case "LAST_VISITED":
|
||||||
return formatRelative(
|
return formatRelative(
|
||||||
fromUnixTime(parseInt(String(value), 10)),
|
fromUnixTime(parseInt(String(value), 10)),
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import { query } from "../../lib/entity";
|
import { query } from "../../lib/entity";
|
||||||
import UpObject from "../display/UpObject.svelte";
|
import UpObject from "../display/UpObject.svelte";
|
||||||
import UpObjectCard from "../display/UpObjectCard.svelte";
|
import UpObjectCard from "../display/UpObjectCard.svelte";
|
||||||
|
import { ATTR_LABEL } from "upend/constants";
|
||||||
|
|
||||||
export let entities: Address[];
|
export let entities: Address[];
|
||||||
export let thumbnails = true;
|
export let thumbnails = true;
|
||||||
|
@ -49,7 +50,9 @@
|
||||||
|
|
||||||
const addressesString = addresses.map((addr) => `@${addr}`).join(" ");
|
const addressesString = addresses.map((addr) => `@${addr}`).join(" ");
|
||||||
|
|
||||||
labelListing = query(`(matches (in ${addressesString}) "LBL" ? )`).result;
|
labelListing = query(
|
||||||
|
`(matches (in ${addressesString}) "${ATTR_LABEL}" ? )`
|
||||||
|
).result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortEntities() {
|
function sortEntities() {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
"ADDED": "Added at",
|
"ADDED": "Added at",
|
||||||
"LAST_VISITED": "Last visited at",
|
"LAST_VISITED": "Last visited at",
|
||||||
"NUM_VISITED": "Times visited",
|
"NUM_VISITED": "Times visited",
|
||||||
"LBL": "Label",
|
"ATTR_LABEL": "Label",
|
||||||
"IS": "Type",
|
"IS": "Type",
|
||||||
"TYPE": "Type ID",
|
"TYPE": "Type ID",
|
||||||
"MEDIA_DURATION": "Duration"
|
"MEDIA_DURATION": "Duration"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { EntityInfo } from "upend/types";
|
import type { EntityInfo } from "upend/types";
|
||||||
import type { UpObject } from "upend";
|
import type { UpObject } from "upend";
|
||||||
|
import { ATTR_IN } from "upend/constants";
|
||||||
|
|
||||||
export function getTypes(entity: UpObject, entityInfo: EntityInfo) {
|
export function getTypes(entity: UpObject, entityInfo: EntityInfo) {
|
||||||
const mimeType = String(entity?.get("FILE_MIME"));
|
const mimeType = String(entity?.get("FILE_MIME"));
|
||||||
|
@ -22,7 +23,7 @@ export function getTypes(entity: UpObject, entityInfo: EntityInfo) {
|
||||||
const web = entityInfo?.t == "Url";
|
const web = entityInfo?.t == "Url";
|
||||||
const fragment = Boolean(entity?.get("ANNOTATES"));
|
const fragment = Boolean(entity?.get("ANNOTATES"));
|
||||||
|
|
||||||
const group = entity?.backlinks.some((e) => e.attribute == "OF");
|
const group = entity?.backlinks.some((e) => e.attribute == ATTR_IN);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mimeType,
|
mimeType,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { PutInput } from "upend/types";
|
import type { PutInput } from "upend/types";
|
||||||
import { query as queryFn } from "../lib/entity";
|
import { query as queryFn } from "../lib/entity";
|
||||||
import api from "../lib/api";
|
import api from "../lib/api";
|
||||||
|
import { ATTR_LABEL } from "upend/constants";
|
||||||
|
|
||||||
export function baseSearch(query: string) {
|
export function baseSearch(query: string) {
|
||||||
return queryFn(
|
return queryFn(
|
||||||
|
@ -26,7 +27,7 @@ export async function createLabelled(label: string) {
|
||||||
} else {
|
} else {
|
||||||
// TODO - don't create invariants, create UUIDs instead, maybe with keys?
|
// TODO - don't create invariants, create UUIDs instead, maybe with keys?
|
||||||
body = {
|
body = {
|
||||||
attribute: "LBL",
|
attribute: ATTR_LABEL,
|
||||||
value: {
|
value: {
|
||||||
t: "String",
|
t: "String",
|
||||||
c: label,
|
c: label,
|
||||||
|
|
|
@ -12,12 +12,13 @@
|
||||||
import { vaultInfo } from "../util/info";
|
import { vaultInfo } from "../util/info";
|
||||||
import { updateTitle } from "../util/title";
|
import { updateTitle } from "../util/title";
|
||||||
import { i18n } from "../i18n";
|
import { i18n } from "../i18n";
|
||||||
|
import { ATTR_ADDED, ATTR_LABEL } from "upend/constants";
|
||||||
|
|
||||||
const roots = (async () => {
|
const roots = (async () => {
|
||||||
const data = await api.fetchRoots();
|
const data = await api.fetchRoots();
|
||||||
const listing = new UpListing(data);
|
const listing = new UpListing(data);
|
||||||
return Object.values(listing.objects)
|
return Object.values(listing.objects)
|
||||||
.filter((obj) => Boolean(obj.attr["LBL"]))
|
.filter((obj) => Boolean(obj.attr[ATTR_LABEL]))
|
||||||
.map((obj) => [obj.address, obj.identify().join(" | ")])
|
.map((obj) => [obj.address, obj.identify().join(" | ")])
|
||||||
.sort(([_, i1], [__, i2]) => i1.localeCompare(i2));
|
.sort(([_, i1], [__, i2]) => i1.localeCompare(i2));
|
||||||
})();
|
})();
|
||||||
|
@ -34,7 +35,7 @@
|
||||||
.sort((a, b) => (b.value.c as number) - (a.value.c as number))
|
.sort((a, b) => (b.value.c as number) - (a.value.c as number))
|
||||||
.slice(0, 25);
|
.slice(0, 25);
|
||||||
|
|
||||||
const { result: latestQuery } = query(`(matches ? "ADDED" ?)`);
|
const { result: latestQuery } = query(`(matches ? "${ATTR_ADDED}" ?)`);
|
||||||
$: latest = ($latestQuery?.entries || [])
|
$: latest = ($latestQuery?.entries || [])
|
||||||
.filter((e) => e.value.t == "Number")
|
.filter((e) => e.value.t == "Number")
|
||||||
.sort((a, b) => (b.value.c as number) - (a.value.c as number))
|
.sort((a, b) => (b.value.c as number) - (a.value.c as number))
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
import api from "../lib/api";
|
import api from "../lib/api";
|
||||||
import Gallery from "../components/widgets/Gallery.svelte";
|
import Gallery from "../components/widgets/Gallery.svelte";
|
||||||
import { matchSorter } from "match-sorter";
|
import { matchSorter } from "match-sorter";
|
||||||
|
import { ATTR_LABEL } from "upend/constants";
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
export let query: string;
|
export let query: string;
|
||||||
|
@ -35,7 +36,9 @@
|
||||||
exactHits = [];
|
exactHits = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$: objects = ($result?.entries || []).filter((e) => e.attribute === "LBL");
|
$: objects = ($result?.entries || []).filter(
|
||||||
|
(e) => e.attribute === ATTR_LABEL
|
||||||
|
);
|
||||||
$: sortedObjects = matchSorter(objects, debouncedQuery, {
|
$: sortedObjects = matchSorter(objects, debouncedQuery, {
|
||||||
keys: ["value.c"],
|
keys: ["value.c"],
|
||||||
});
|
});
|
||||||
|
@ -44,7 +47,7 @@
|
||||||
$: {
|
$: {
|
||||||
const addressesString = objects.map((e) => `@${e.entity}`).join(" ");
|
const addressesString = objects.map((e) => `@${e.entity}`).join(" ");
|
||||||
api
|
api
|
||||||
.query(`(matches (in ${addressesString}) "LBL" ? )`)
|
.query(`(matches (in ${addressesString}) "${ATTR_LABEL}" ? )`)
|
||||||
.then((labelListing) => {
|
.then((labelListing) => {
|
||||||
exactHits = labelListing.entries
|
exactHits = labelListing.entries
|
||||||
.filter(
|
.filter(
|
||||||
|
|
|
@ -12585,11 +12585,11 @@ __metadata:
|
||||||
|
|
||||||
"upend@file:../tools/upend_js::locator=upend-kestrel%40workspace%3A.":
|
"upend@file:../tools/upend_js::locator=upend-kestrel%40workspace%3A.":
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
resolution: "upend@file:../tools/upend_js#../tools/upend_js::hash=88616b&locator=upend-kestrel%40workspace%3A."
|
resolution: "upend@file:../tools/upend_js#../tools/upend_js::hash=cda57f&locator=upend-kestrel%40workspace%3A."
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: ^4.3.4
|
debug: ^4.3.4
|
||||||
lru-cache: ^7.0.0
|
lru-cache: ^7.0.0
|
||||||
checksum: c73ce133f42c9669f15b5d38b2d552722d9df56ec1daa61d31b41d8a0ec6c9064a9424c6e5589d53edeebfc75c447464f10110539b8237c19bf9597987bd6d06
|
checksum: 551abb5f6c2d07e1350993d27ca835fea005172ad66889e41fa5a9793ad414788ecfce2c966f526d3347d08e1731f1945c695e95aae15dd334ca17b1bc1fd195
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue