@xnetjs/data 2.4.0 → 3.0.0
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 +5 -0
- package/dist/auth/index.d.ts +4 -4
- package/dist/{chunk-56EXPOJ4.js → chunk-ANMAR3KY.js} +34 -19
- package/dist/chunk-GJAOMEQO.js +114 -0
- package/dist/chunk-KY62NUOI.js +67 -0
- package/dist/chunk-N37U6ZPI.js +789 -0
- package/dist/chunk-NFPEFD2I.js +42 -0
- package/dist/{chunk-EGGRKSRH.js → chunk-QKVIKVAL.js} +70 -31
- package/dist/{chunk-CY6EJUNV.js → chunk-S7SEN4LH.js} +5 -1
- package/dist/{chunk-4HJZJMPI.js → chunk-TN7LGV66.js} +1 -1
- package/dist/{chunk-YRFCXKJC.js → chunk-UQ2VFHU3.js} +2 -2
- package/dist/chunk-VZSEZNYA.js +83 -0
- package/dist/chunk-WSINZHSR.js +90 -0
- package/dist/{chunk-CNGEXCIH.js → chunk-YCBJDT23.js} +69 -55
- package/dist/{clone-BsoRt89i.d.ts → clone-ywzFVkID.d.ts} +16 -6
- package/dist/course-C5DXOIVO.js +24 -0
- package/dist/database/index.d.ts +17 -5
- package/dist/database/index.js +7 -7
- package/dist/event-VW3FBN4F.js +20 -0
- package/dist/{form-types-Dkqm9UO2.d.ts → form-types-BrHYdpi0.d.ts} +1 -1
- package/dist/{game-NEQP7OFX.js → game-7AXW7XKS.js} +1 -1
- package/dist/{grant-expiration-cleaner-BwdmRFua.d.ts → grant-expiration-cleaner-CRS9OyhV.d.ts} +3 -3
- package/dist/index.d.ts +10 -320
- package/dist/index.js +522 -1097
- package/dist/{moderation-HRMMBFVH.js → moderation-5Z4LKOKN.js} +5 -3
- package/dist/{page-RUS2BKMX.js → page-W4G6433H.js} +1 -1
- package/dist/portability/index.d.ts +330 -0
- package/dist/portability/index.js +34 -0
- package/dist/post-7IOO64MD.js +14 -0
- package/dist/{profile-5H6XWCK4.js → profile-KLHCAQY2.js} +1 -1
- package/dist/publication-3MN3WWKP.js +10 -0
- package/dist/{query-ast-DYGcKxoa.d.ts → query-ast-DQJO7yRV.d.ts} +1 -1
- package/dist/{registry-D_3b1Lg6.d.ts → registry-C9IRLLeR.d.ts} +1 -1
- package/dist/schema/index.d.ts +375 -26
- package/dist/schema/index.js +213 -167
- package/dist/space-GBTEHZOK.js +1 -1
- package/dist/store/index.d.ts +4 -4
- package/dist/store/index.js +4 -4
- package/dist/{store-BMnhBWzM.d.ts → store-D1oiFQuF.d.ts} +19 -2
- package/dist/{types-cycApzib.d.ts → types-gws1tSf-.d.ts} +1 -1
- package/package.json +27 -25
- package/dist/chunk-Y2XWK7AA.js +0 -50
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Schema system, NodeStore, and Yjs CRDT engine for xNet. This is the central data package -- it defines how structured data and rich text documents are created, stored, and synced.
|
|
4
4
|
|
|
5
|
+
> **Alpha software.** xNet is released but early: this package is on npm and
|
|
6
|
+
> usable today, but its API can change between releases, sometimes without a
|
|
7
|
+
> migration path. Pin your version. See the
|
|
8
|
+
> [project README](https://github.com/crs48/xNet#readme) for what alpha means here.
|
|
9
|
+
|
|
5
10
|
> **Status:** Mixed
|
|
6
11
|
> Stable entrypoints: `@xnetjs/data/schema`, `@xnetjs/data/store`, `@xnetjs/data/updates`, `@xnetjs/data/awareness`
|
|
7
12
|
> Experimental entrypoints: `@xnetjs/data/database`, `@xnetjs/data/auth`
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { AllowExpr, DenyExpr, AuthExpression, AndExpr, OrExpr, NotExpr, PublicExpr, AuthenticatedExpr, CreatorRoleResolver, PropertyRoleResolver, RelationRoleResolver, MembershipRoleResolver, AuthorizationDefinition, SerializedAuthorization, SerializedAuthExpression, DID } from '@xnetjs/core';
|
|
2
2
|
export { AUTH_ACTIONS, AllowExpr, AndExpr, AuthAction, AuthCheckInput, AuthDecision, AuthDenyReason, AuthExpression, AuthTrace, AuthTraceStep, AuthenticatedExpr, AuthorizationDefinition, CreatorRoleResolver, DenyExpr, MembershipRoleResolver, NotExpr, OrExpr, PolicyEvaluator, PropertyRoleResolver, PublicExpr, RelationRoleResolver, RoleRefExpr, RoleResolver, SerializedAuthExpression, SerializedAuthorization, SerializedRoleResolver } from '@xnetjs/core';
|
|
3
|
-
import {
|
|
4
|
-
export {
|
|
5
|
-
export { A as ActionPermission, m as AuthDecisionEvent,
|
|
3
|
+
import { F as PropertyDefinition, a as Schema, h as NodeState, S as SchemaIRI } from '../types-gws1tSf-.js';
|
|
4
|
+
export { a_ as DEFAULT_OFFLINE_POLICY, aY as GRANT_SCHEMA_IRI, b4 as Grant, x as GrantIndex, b9 as GrantIndexOptions, b8 as GrantIndexStore, b3 as GrantInput, b7 as GrantNode, aX as GrantRateLimiter, bc as GrantRateLimiterOptions, O as OfflineAuthPolicy, bb as RevocationConfig, ba as RevocationConsistency, aW as StoreAuth, e as StoreAuthAPI, b5 as StoreAuthError, b6 as StoreAuthErrorCode, b2 as StoreAuthKeyManager, b0 as StoreAuthOptions, b1 as StoreAuthStore, aZ as isGrantActive, a$ as mergeOfflinePolicy } from '../types-gws1tSf-.js';
|
|
5
|
+
export { A as ActionPermission, m as AuthDecisionEvent, b as DecisionCache, g as DecisionCacheOptions, D as DefaultPolicyEvaluator, j as DefaultPolicyEvaluatorOptions, a as DefaultRoleResolver, i as DefaultRoleResolverOptions, k as DefinedSchemaLike, G as GrantExpirationCleaner, o as GrantExpirationCleanerOptions, n as GrantExpirationStore, H as HubPolicy, N as NodeStoreReader, P as PermissionMatrix, l as RevalidationEvent, R as RoleSummary, S as SchemaRegistryReader, c as buildPermissionMatrix, f as createPolicyEvaluator, d as describeRoleResolver, e as evaluateExpression, h as hubActionsForSpaceRole, s as schemaToHubPolicy } from '../grant-expiration-cleaner-CRS9OyhV.js';
|
|
6
6
|
import { PublicKeyResolver } from '@xnetjs/crypto';
|
|
7
7
|
import '@xnetjs/sqlite';
|
|
8
8
|
import '@xnetjs/sync';
|
|
9
|
-
import '../registry-
|
|
9
|
+
import '../registry-C9IRLLeR.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Authorization expression builders for schema definitions.
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
extKey
|
|
3
3
|
} from "./chunk-PMUQACPY.js";
|
|
4
|
+
import {
|
|
5
|
+
DatabaseRowSchema
|
|
6
|
+
} from "./chunk-AKKBG7HW.js";
|
|
7
|
+
import {
|
|
8
|
+
DatabaseFieldSchema
|
|
9
|
+
} from "./chunk-IKREWSCX.js";
|
|
10
|
+
import {
|
|
11
|
+
DatabaseSelectOptionSchema
|
|
12
|
+
} from "./chunk-XP3VSSXD.js";
|
|
13
|
+
import {
|
|
14
|
+
DatabaseViewSchema
|
|
15
|
+
} from "./chunk-UFV5QWCL.js";
|
|
4
16
|
import {
|
|
5
17
|
ExtensionFieldSchema,
|
|
6
18
|
SchemaExtensionSchema,
|
|
@@ -14,18 +26,6 @@ import {
|
|
|
14
26
|
needsRebalancing,
|
|
15
27
|
rebalanceSortKeys
|
|
16
28
|
} from "./chunk-QWFTRZQT.js";
|
|
17
|
-
import {
|
|
18
|
-
DatabaseRowSchema
|
|
19
|
-
} from "./chunk-AKKBG7HW.js";
|
|
20
|
-
import {
|
|
21
|
-
DatabaseFieldSchema
|
|
22
|
-
} from "./chunk-IKREWSCX.js";
|
|
23
|
-
import {
|
|
24
|
-
DatabaseSelectOptionSchema
|
|
25
|
-
} from "./chunk-XP3VSSXD.js";
|
|
26
|
-
import {
|
|
27
|
-
DatabaseViewSchema
|
|
28
|
-
} from "./chunk-UFV5QWCL.js";
|
|
29
29
|
import {
|
|
30
30
|
createNodeQueryDescriptor
|
|
31
31
|
} from "./chunk-WOEFYS7Y.js";
|
|
@@ -3384,6 +3384,18 @@ function evaluateUnary(operator, operand, context) {
|
|
|
3384
3384
|
}
|
|
3385
3385
|
}
|
|
3386
3386
|
function evaluateCall(name, args, context) {
|
|
3387
|
+
if (name === "RELATED") {
|
|
3388
|
+
const evaluated = args.map((arg) => evaluate(arg, context));
|
|
3389
|
+
const relationColumnId = String(evaluated[0] ?? "");
|
|
3390
|
+
const targetColumnId = evaluated[1] === void 0 ? void 0 : String(evaluated[1]);
|
|
3391
|
+
return context.getRelatedValues?.(relationColumnId, targetColumnId) ?? [];
|
|
3392
|
+
}
|
|
3393
|
+
if (name === "NODE") {
|
|
3394
|
+
const evaluated = args.map((arg) => evaluate(arg, context));
|
|
3395
|
+
const nodeId = String(evaluated[0] ?? "");
|
|
3396
|
+
const property = String(evaluated[1] ?? "");
|
|
3397
|
+
return context.getNodeProperty?.(nodeId, property) ?? null;
|
|
3398
|
+
}
|
|
3387
3399
|
const fn = FUNCTIONS[name];
|
|
3388
3400
|
if (!fn) {
|
|
3389
3401
|
throw new Error(`Unknown function: ${name}`);
|
|
@@ -3585,15 +3597,16 @@ var FormulaService = class {
|
|
|
3585
3597
|
* const value = service.compute(row, formulaColumn, columns)
|
|
3586
3598
|
* ```
|
|
3587
3599
|
*/
|
|
3588
|
-
compute(row, column, columns) {
|
|
3600
|
+
compute(row, column, columns, scope) {
|
|
3589
3601
|
if (column.type !== "formula") {
|
|
3590
3602
|
throw new Error(`Column ${column.id} is not a formula column`);
|
|
3591
3603
|
}
|
|
3592
3604
|
const config = column.config;
|
|
3605
|
+
const usesScope = /\b(RELATED|NODE)\s*\(/.test(config.expression);
|
|
3593
3606
|
const cacheKey = `${row.id}:${column.id}`;
|
|
3594
3607
|
const deps = config.dependencies ?? extractDependencies(config.expression);
|
|
3595
3608
|
const hash = this.computeHash(row, deps);
|
|
3596
|
-
const cached = this.valueCache.get(cacheKey);
|
|
3609
|
+
const cached = usesScope ? void 0 : this.valueCache.get(cacheKey);
|
|
3597
3610
|
if (cached && cached.hash === hash) {
|
|
3598
3611
|
return cached.value;
|
|
3599
3612
|
}
|
|
@@ -3606,16 +3619,18 @@ var FormulaService = class {
|
|
|
3606
3619
|
getValue: (columnId) => {
|
|
3607
3620
|
const depColumn = columns.find((c) => c.id === columnId);
|
|
3608
3621
|
if (depColumn?.type === "formula") {
|
|
3609
|
-
return this.compute(row, depColumn, columns);
|
|
3622
|
+
return this.compute(row, depColumn, columns, scope);
|
|
3610
3623
|
}
|
|
3611
3624
|
return row.cells[columnId];
|
|
3612
3625
|
},
|
|
3613
|
-
getColumn: (columnId) => columns.find((c) => c.id === columnId)
|
|
3626
|
+
getColumn: (columnId) => columns.find((c) => c.id === columnId),
|
|
3627
|
+
getRelatedValues: scope?.getRelatedValues,
|
|
3628
|
+
getNodeProperty: scope?.getNodeProperty
|
|
3614
3629
|
};
|
|
3615
3630
|
try {
|
|
3616
3631
|
const value = evaluate(ast, context);
|
|
3617
3632
|
const coerced = this.coerceResult(value, config.resultType);
|
|
3618
|
-
this.valueCache.set(cacheKey, { value: coerced, hash });
|
|
3633
|
+
if (!usesScope) this.valueCache.set(cacheKey, { value: coerced, hash });
|
|
3619
3634
|
return coerced;
|
|
3620
3635
|
} catch (error) {
|
|
3621
3636
|
console.error(`Formula error in ${column.name}:`, error);
|
|
@@ -3625,10 +3640,10 @@ var FormulaService = class {
|
|
|
3625
3640
|
/**
|
|
3626
3641
|
* Batch compute formulas for multiple rows.
|
|
3627
3642
|
*/
|
|
3628
|
-
batchCompute(rows, column, columns) {
|
|
3643
|
+
batchCompute(rows, column, columns, scope) {
|
|
3629
3644
|
const results = /* @__PURE__ */ new Map();
|
|
3630
3645
|
for (const row of rows) {
|
|
3631
|
-
const value = this.compute(row, column, columns);
|
|
3646
|
+
const value = this.compute(row, column, columns, scope);
|
|
3632
3647
|
results.set(row.id, value);
|
|
3633
3648
|
}
|
|
3634
3649
|
return results;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
spaceCascadeAuthorization
|
|
3
|
+
} from "./chunk-RLRAWZJE.js";
|
|
4
|
+
import {
|
|
5
|
+
checkbox,
|
|
6
|
+
created,
|
|
7
|
+
createdBy,
|
|
8
|
+
defineSchema,
|
|
9
|
+
number,
|
|
10
|
+
relation,
|
|
11
|
+
text
|
|
12
|
+
} from "./chunk-NH6OLO3X.js";
|
|
13
|
+
import {
|
|
14
|
+
allow,
|
|
15
|
+
role
|
|
16
|
+
} from "./chunk-D4LUUZYD.js";
|
|
17
|
+
|
|
18
|
+
// src/schema/schemas/course.ts
|
|
19
|
+
var COURSE_SCHEMA_IRI = "xnet://xnet.fyi/Course@1.0.0";
|
|
20
|
+
var LESSON_SCHEMA_IRI = "xnet://xnet.fyi/Lesson@1.0.0";
|
|
21
|
+
var LESSON_PROGRESS_SCHEMA_IRI = "xnet://xnet.fyi/LessonProgress@1.0.0";
|
|
22
|
+
var CourseSchema = defineSchema({
|
|
23
|
+
name: "Course",
|
|
24
|
+
namespace: "xnet://xnet.fyi/",
|
|
25
|
+
properties: {
|
|
26
|
+
title: text({ required: true, maxLength: 300 }),
|
|
27
|
+
description: text({ maxLength: 2e3 }),
|
|
28
|
+
space: relation({ target: "xnet://xnet.fyi/Space@1.0.0" }),
|
|
29
|
+
icon: text({ maxLength: 500 }),
|
|
30
|
+
/** Order among sibling courses — fractional index, code-unit compare. */
|
|
31
|
+
sortKey: text({ maxLength: 500 }),
|
|
32
|
+
/** Unpublished courses are visible to space admins only. */
|
|
33
|
+
published: checkbox({ default: false }),
|
|
34
|
+
createdAt: created(),
|
|
35
|
+
createdBy: createdBy()
|
|
36
|
+
},
|
|
37
|
+
document: void 0,
|
|
38
|
+
authorization: spaceCascadeAuthorization()
|
|
39
|
+
});
|
|
40
|
+
var LessonSchema = defineSchema({
|
|
41
|
+
name: "Lesson",
|
|
42
|
+
namespace: "xnet://xnet.fyi/",
|
|
43
|
+
properties: {
|
|
44
|
+
title: text({ required: true, maxLength: 300 }),
|
|
45
|
+
course: relation({ target: "xnet://xnet.fyi/Course@1.0.0", required: true }),
|
|
46
|
+
space: relation({ target: "xnet://xnet.fyi/Space@1.0.0" }),
|
|
47
|
+
/** Order within the course — fractional index. */
|
|
48
|
+
sortKey: text({ maxLength: 500 }),
|
|
49
|
+
/** Optional estimated minutes, shown as guidance, never as a timer. */
|
|
50
|
+
estimatedMinutes: number({ min: 0 }),
|
|
51
|
+
createdAt: created(),
|
|
52
|
+
createdBy: createdBy()
|
|
53
|
+
},
|
|
54
|
+
// Lesson bodies are documents, same editor surface as Page/Post.
|
|
55
|
+
document: "yjs",
|
|
56
|
+
authorization: spaceCascadeAuthorization()
|
|
57
|
+
});
|
|
58
|
+
var LessonProgressSchema = defineSchema({
|
|
59
|
+
name: "LessonProgress",
|
|
60
|
+
namespace: "xnet://xnet.fyi/",
|
|
61
|
+
properties: {
|
|
62
|
+
lesson: relation({ target: "xnet://xnet.fyi/Lesson@1.0.0", required: true }),
|
|
63
|
+
course: relation({ target: "xnet://xnet.fyi/Course@1.0.0" }),
|
|
64
|
+
space: relation({ target: "xnet://xnet.fyi/Space@1.0.0" }),
|
|
65
|
+
/** ms since epoch; absent = started but not finished. */
|
|
66
|
+
completedAt: number({}),
|
|
67
|
+
createdAt: created(),
|
|
68
|
+
createdBy: createdBy()
|
|
69
|
+
},
|
|
70
|
+
document: void 0,
|
|
71
|
+
// Written out rather than derived from the space cascade, because the whole
|
|
72
|
+
// point is that it does NOT cascade read to the membership.
|
|
73
|
+
authorization: {
|
|
74
|
+
roles: {
|
|
75
|
+
owner: role.creator(),
|
|
76
|
+
spaceOwner: role.relation("space", "spaceOwner"),
|
|
77
|
+
spaceAdmin: role.relation("space", "spaceAdmin"),
|
|
78
|
+
spaceMember: role.relation("space", "spaceMember")
|
|
79
|
+
},
|
|
80
|
+
actions: {
|
|
81
|
+
// Private by construction: the learner and the space's stewards, and
|
|
82
|
+
// nobody else. There is no membership-wide read of someone's progress.
|
|
83
|
+
read: allow("owner", "spaceOwner", "spaceAdmin"),
|
|
84
|
+
create: allow("spaceOwner", "spaceAdmin", "spaceMember"),
|
|
85
|
+
update: allow("owner"),
|
|
86
|
+
delete: allow("owner", "spaceOwner", "spaceAdmin"),
|
|
87
|
+
write: allow("owner"),
|
|
88
|
+
share: allow("owner")
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
function lessonProgressId(lessonId, learnerDid) {
|
|
93
|
+
return `lessonprogress:${lessonId}:${learnerDid}`;
|
|
94
|
+
}
|
|
95
|
+
var courseCompletion = (lessons, ownProgress) => {
|
|
96
|
+
const done = new Set(
|
|
97
|
+
ownProgress.filter((p) => typeof p.completedAt === "number" && p.completedAt > 0).map((p) => p.lesson)
|
|
98
|
+
);
|
|
99
|
+
return {
|
|
100
|
+
completed: lessons.filter((l) => done.has(l.id)).length,
|
|
101
|
+
total: lessons.length
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
COURSE_SCHEMA_IRI,
|
|
107
|
+
LESSON_SCHEMA_IRI,
|
|
108
|
+
LESSON_PROGRESS_SCHEMA_IRI,
|
|
109
|
+
CourseSchema,
|
|
110
|
+
LessonSchema,
|
|
111
|
+
LessonProgressSchema,
|
|
112
|
+
lessonProgressId,
|
|
113
|
+
courseCompletion
|
|
114
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
spaceContributorAuthorization
|
|
3
|
+
} from "./chunk-RLRAWZJE.js";
|
|
4
|
+
import {
|
|
5
|
+
checkbox,
|
|
6
|
+
created,
|
|
7
|
+
createdBy,
|
|
8
|
+
defineSchema,
|
|
9
|
+
relation,
|
|
10
|
+
select,
|
|
11
|
+
text
|
|
12
|
+
} from "./chunk-NH6OLO3X.js";
|
|
13
|
+
|
|
14
|
+
// src/schema/schemas/post.ts
|
|
15
|
+
var POST_SCHEMA_IRI = "xnet://xnet.fyi/Post@1.0.0";
|
|
16
|
+
var PostSchema = defineSchema({
|
|
17
|
+
name: "Post",
|
|
18
|
+
namespace: "xnet://xnet.fyi/",
|
|
19
|
+
properties: {
|
|
20
|
+
/** Topic title — what the discussion is about. */
|
|
21
|
+
title: text({ required: true, maxLength: 300 }),
|
|
22
|
+
/** Canonical SECURITY home. A Post without a Space is a personal draft. */
|
|
23
|
+
space: relation({ target: "xnet://xnet.fyi/Space@1.0.0" }),
|
|
24
|
+
/**
|
|
25
|
+
* Optional grouping within a community — "Announcements", "Wins",
|
|
26
|
+
* "Questions". A plain label rather than a relation: communities rename
|
|
27
|
+
* and merge these constantly, and a Tag relation would make the common
|
|
28
|
+
* case (one host, five categories) a two-node write.
|
|
29
|
+
*/
|
|
30
|
+
category: text({ maxLength: 80 }),
|
|
31
|
+
/**
|
|
32
|
+
* Pinned topics sort above the rest. This is the ONE ordering lever, and
|
|
33
|
+
* it is an explicit editorial act by a space admin — not an engagement
|
|
34
|
+
* score, and not computed from any behavioural signal.
|
|
35
|
+
*/
|
|
36
|
+
pinned: checkbox({ default: false }),
|
|
37
|
+
/** Locked topics accept no new replies; existing ones stay readable. */
|
|
38
|
+
locked: checkbox({ default: false }),
|
|
39
|
+
/** Per-node visibility; `inherit` defers to the Space (exploration 0179). */
|
|
40
|
+
visibility: select({
|
|
41
|
+
options: [
|
|
42
|
+
{ id: "inherit", name: "Inherit", color: "gray" },
|
|
43
|
+
{ id: "private", name: "Private", color: "gray" },
|
|
44
|
+
{ id: "unlisted", name: "Unlisted", color: "yellow" },
|
|
45
|
+
{ id: "public", name: "Public", color: "green" }
|
|
46
|
+
],
|
|
47
|
+
default: "inherit"
|
|
48
|
+
}),
|
|
49
|
+
createdAt: created(),
|
|
50
|
+
createdBy: createdBy()
|
|
51
|
+
},
|
|
52
|
+
// Rich body, same editor surface as Page (BlockNote over a Y.Doc).
|
|
53
|
+
document: "yjs",
|
|
54
|
+
authorization: spaceContributorAuthorization()
|
|
55
|
+
});
|
|
56
|
+
var comparePostsForFeed = (a, b) => {
|
|
57
|
+
const aPinned = a.pinned === true;
|
|
58
|
+
const bPinned = b.pinned === true;
|
|
59
|
+
if (aPinned !== bPinned) return aPinned ? -1 : 1;
|
|
60
|
+
return (b.createdAt ?? 0) - (a.createdAt ?? 0);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
POST_SCHEMA_IRI,
|
|
65
|
+
PostSchema,
|
|
66
|
+
comparePostsForFeed
|
|
67
|
+
};
|