mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-24 10:18:11 +00:00
Add a ledger/user type (#1915)
This adds a user type to the ledger. Each user is tracked by a UUID, and has a human-specified and renameable username. Each user also corresponds to a node in the graph, and has a list of aliase addresses that should be merged into it using `graph.contractNodes`. It's important that we track this in the ledger because the ledger needs to reconcile when aliases are added or removed. Test plan: Unit tests added. Quite a simple module.
This commit is contained in:
parent
43d378ca93
commit
e9c70d5327
105
src/ledger/user.js
Normal file
105
src/ledger/user.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This module has a core data type identifying SourceCred users.
|
||||||
|
*
|
||||||
|
* The scope for this data type is to model:
|
||||||
|
* - a unique identifier for each user
|
||||||
|
* - a unique (renameable) username they choose
|
||||||
|
* - the address of every node they correspond to in the graph
|
||||||
|
*
|
||||||
|
* Unlike most other state in SourceCred, the User state is
|
||||||
|
* nondeterministically generated by SourceCred itself, and then persisted
|
||||||
|
* long-term within the instance.
|
||||||
|
*
|
||||||
|
* This is in contrast to Graph data, which usually comes from an external
|
||||||
|
* source, and is not persisted long-term, but instead is re-generated when
|
||||||
|
* needed.
|
||||||
|
*
|
||||||
|
* In particular, this kernel of user data is stored within the core ledger,
|
||||||
|
* since it's necessary to track consistently when tracking Grain distribution.
|
||||||
|
* This type should not grow to include all the data that the UI will
|
||||||
|
* eventually want to show; that should be kept in a different data store which
|
||||||
|
* isn't being used as a transaction ledger.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
type Uuid,
|
||||||
|
parser as uuidParser,
|
||||||
|
random as randomUuid,
|
||||||
|
} from "../util/uuid";
|
||||||
|
import * as C from "../util/combo";
|
||||||
|
import {
|
||||||
|
type NodeAddressT,
|
||||||
|
NodeAddress,
|
||||||
|
type Node as GraphNode,
|
||||||
|
} from "../core/graph";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We validate usernames using GitHub-esque rules.
|
||||||
|
*/
|
||||||
|
export opaque type Username: string = string;
|
||||||
|
const USERNAME_PATTERN = /^@?([A-Za-z0-9-_]+)$/;
|
||||||
|
|
||||||
|
export type UserId = Uuid;
|
||||||
|
export type User = {|
|
||||||
|
// UUID, assigned when the user is created.
|
||||||
|
+id: UserId,
|
||||||
|
+name: Username,
|
||||||
|
// Every other node in the graph that this user corresponds to.
|
||||||
|
// Does not include the user's "own" address, i.e. the result
|
||||||
|
// of calling (userAddress(user.id)).
|
||||||
|
+aliases: $ReadOnlyArray<NodeAddressT>,
|
||||||
|
|};
|
||||||
|
|
||||||
|
// It's not in the typical [owner, name] format because it isn't provided by a plugin.
|
||||||
|
// Instead, it's a raw type owned by SourceCred project.
|
||||||
|
export const USER_PREFIX = NodeAddress.fromParts(["sourcecred", "USER"]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user, assigning a random id.
|
||||||
|
*/
|
||||||
|
export function createUser(name: string): User {
|
||||||
|
return {
|
||||||
|
id: randomUuid(),
|
||||||
|
name,
|
||||||
|
aliases: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a Username from a string.
|
||||||
|
*
|
||||||
|
* Throws an error if the username is invalid.
|
||||||
|
*/
|
||||||
|
export function usernameFromString(username: string): Username {
|
||||||
|
const re = new RegExp(USERNAME_PATTERN);
|
||||||
|
const match = re.exec(username);
|
||||||
|
if (match == null) {
|
||||||
|
throw new Error(`invalid username: ${username}`);
|
||||||
|
}
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userAddress(id: UserId): NodeAddressT {
|
||||||
|
return NodeAddress.append(USER_PREFIX, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function graphNode({id, name}: User): GraphNode {
|
||||||
|
return {
|
||||||
|
address: userAddress(id),
|
||||||
|
description: name,
|
||||||
|
timestampMs: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usernameParser: C.Parser<Username> = C.fmap(
|
||||||
|
C.string,
|
||||||
|
usernameFromString
|
||||||
|
);
|
||||||
|
|
||||||
|
export const userParser: C.Parser<User> = C.object({
|
||||||
|
id: uuidParser,
|
||||||
|
name: usernameParser,
|
||||||
|
aliases: C.array(C.fmap(C.string, NodeAddress.fromRaw)),
|
||||||
|
});
|
53
src/ledger/user.test.js
Normal file
53
src/ledger/user.test.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import deepFreeze from "deep-freeze";
|
||||||
|
import {fromString as uuidFromString} from "../util/uuid";
|
||||||
|
import {NodeAddress} from "../core/graph";
|
||||||
|
import {
|
||||||
|
createUser,
|
||||||
|
userAddress,
|
||||||
|
usernameFromString,
|
||||||
|
USER_PREFIX,
|
||||||
|
graphNode,
|
||||||
|
type User,
|
||||||
|
} from "./user";
|
||||||
|
|
||||||
|
describe("ledger/user", () => {
|
||||||
|
const uuid = uuidFromString("YVZhbGlkVXVpZEF0TGFzdA");
|
||||||
|
const name = usernameFromString("foo");
|
||||||
|
const example: User = deepFreeze({
|
||||||
|
id: uuid,
|
||||||
|
name,
|
||||||
|
aliases: [NodeAddress.empty],
|
||||||
|
});
|
||||||
|
it("createUser works", () => {
|
||||||
|
const user = createUser(name);
|
||||||
|
expect(user.aliases).toEqual([]);
|
||||||
|
expect(user.name).toEqual(name);
|
||||||
|
// Verify it is a valid UUID
|
||||||
|
uuidFromString(user.id);
|
||||||
|
});
|
||||||
|
it("userAddress works", () => {
|
||||||
|
expect(userAddress(uuid)).toEqual(NodeAddress.append(USER_PREFIX, uuid));
|
||||||
|
});
|
||||||
|
it("graphNode works", () => {
|
||||||
|
const node = graphNode(example);
|
||||||
|
expect(node.description).toEqual(example.name);
|
||||||
|
expect(node.address).toEqual(userAddress(uuid));
|
||||||
|
expect(node.timestampMs).toEqual(null);
|
||||||
|
});
|
||||||
|
describe("usernameFromString", () => {
|
||||||
|
it("fails on invalid usernames", () => {
|
||||||
|
const bad = ["With Space", "With.Period", "A/Slash", ""];
|
||||||
|
for (const b of bad) {
|
||||||
|
expect(() => usernameFromString(b)).toThrowError("invalid username");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it("succeeds on valid usernames", () => {
|
||||||
|
const names = ["h", "hi_there", "ZaX99324cab"];
|
||||||
|
for (const n of names) {
|
||||||
|
expect(usernameFromString(n)).toEqual(n);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user