mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-24 02:08:09 +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