@sqaoss/flowy 1.13.0 → 1.13.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqaoss/flowy",
3
- "version": "1.13.0",
3
+ "version": "1.13.1",
4
4
  "description": "Agentic persistent planning",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1395,21 +1395,33 @@ describe('createResolvers', () => {
1395
1395
  })
1396
1396
 
1397
1397
  describe('Mutation.createEdge — edge cases', () => {
1398
- it('throws on duplicate edge', () => {
1398
+ it('is idempotent: creating the same edge twice succeeds and yields one edge', () => {
1399
1399
  const p = create(resolvers, { type: 'project', title: 'P' })
1400
1400
  const f = create(resolvers, { type: 'feature', title: 'F' })
1401
- resolvers.Mutation.createEdge(null, {
1401
+ const first = resolvers.Mutation.createEdge(null, {
1402
1402
  sourceId: f.id,
1403
1403
  targetId: p.id,
1404
1404
  relation: 'part_of',
1405
1405
  })
1406
- expect(() =>
1407
- resolvers.Mutation.createEdge(null, {
1408
- sourceId: f.id,
1409
- targetId: p.id,
1410
- relation: 'part_of',
1411
- }),
1412
- ).toThrow()
1406
+ const second = resolvers.Mutation.createEdge(null, {
1407
+ sourceId: f.id,
1408
+ targetId: p.id,
1409
+ relation: 'part_of',
1410
+ })
1411
+ // Both calls succeed and return the same canonical edge (same created_at).
1412
+ expect(second).toMatchObject({
1413
+ sourceId: f.id,
1414
+ targetId: p.id,
1415
+ relation: 'part_of',
1416
+ })
1417
+ expect(second.createdAt).toBe(first.createdAt)
1418
+ // Exactly one row exists for the triple — no duplicate.
1419
+ const { n } = db.raw
1420
+ .query(
1421
+ 'SELECT COUNT(*) AS n FROM edges WHERE source_id = ? AND target_id = ? AND relation = ?',
1422
+ )
1423
+ .get(f.id, p.id, 'part_of') as { n: number }
1424
+ expect(n).toBe(1)
1413
1425
  })
1414
1426
 
1415
1427
  it('throws when source node does not exist', () => {
@@ -778,25 +778,39 @@ export function createResolvers(db: Db, opts: ResolverOptions = {}) {
778
778
  throw validationError('A node cannot block itself')
779
779
  }
780
780
  const now = new Date().toISOString()
781
+ // Idempotent (F29): re-creating an existing edge is a no-op success, not
782
+ // a PK-violation error. ON CONFLICT DO NOTHING leaves the original row
783
+ // (and its created_at) untouched; we only audit a genuinely new edge.
781
784
  db.raw.transaction(() => {
782
- db.raw.run(
783
- 'INSERT INTO edges (source_id, target_id, relation, created_at) VALUES (?, ?, ?, ?)',
785
+ const result = db.raw.run(
786
+ 'INSERT INTO edges (source_id, target_id, relation, created_at) VALUES (?, ?, ?, ?) ON CONFLICT (source_id, target_id, relation) DO NOTHING',
784
787
  [args.sourceId, args.targetId, args.relation, now],
785
788
  )
786
- // Record the edge against its source node so `auditLog(sourceId)`
787
- // surfaces it. field = relation; newValue = the target it now links.
788
- insertAudit(db, {
789
- nodeId: args.sourceId,
790
- action: 'create_edge',
791
- field: args.relation,
792
- newValue: args.targetId,
793
- })
789
+ if (result.changes > 0) {
790
+ // Record the edge against its source node so `auditLog(sourceId)`
791
+ // surfaces it. field = relation; newValue = the target it now links.
792
+ insertAudit(db, {
793
+ nodeId: args.sourceId,
794
+ action: 'create_edge',
795
+ field: args.relation,
796
+ newValue: args.targetId,
797
+ })
798
+ }
794
799
  })()
800
+ // Read back the canonical row so the returned createdAt reflects the
801
+ // existing edge on a duplicate call, not the discarded `now`.
802
+ const edge = db.raw
803
+ .query(
804
+ 'SELECT created_at FROM edges WHERE source_id = ? AND target_id = ? AND relation = ?',
805
+ )
806
+ .get(args.sourceId, args.targetId, args.relation) as {
807
+ created_at: string
808
+ }
795
809
  return {
796
810
  sourceId: args.sourceId,
797
811
  targetId: args.targetId,
798
812
  relation: args.relation,
799
- createdAt: now,
813
+ createdAt: edge.created_at,
800
814
  }
801
815
  },
802
816